mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 03:57:32 +00:00
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:
@@ -3,23 +3,34 @@ package auth
|
||||
import (
|
||||
"context"
|
||||
"github.com/caos/zitadel/internal/api/auth"
|
||||
authz_repo "github.com/caos/zitadel/internal/authz/repository/eventsourcing"
|
||||
)
|
||||
|
||||
const (
|
||||
adminName = "Admin-API"
|
||||
)
|
||||
|
||||
type TokenVerifier struct {
|
||||
adminID string
|
||||
authZRepo *authz_repo.EsRepository
|
||||
}
|
||||
|
||||
func Start() (v *TokenVerifier) {
|
||||
return new(TokenVerifier)
|
||||
func Start(authZRepo *authz_repo.EsRepository) (v *TokenVerifier) {
|
||||
return &TokenVerifier{authZRepo: authZRepo}
|
||||
}
|
||||
|
||||
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) {
|
||||
return nil, nil
|
||||
func (v *TokenVerifier) ResolveGrant(ctx context.Context) (*auth.Grant, error) {
|
||||
return v.authZRepo.ResolveGrants(ctx)
|
||||
}
|
||||
|
||||
func (v *TokenVerifier) GetProjectIDByClientID(ctx context.Context, clientID string) (string, error) {
|
||||
return "", nil
|
||||
return v.authZRepo.ProjectIDByClientID(ctx, clientID)
|
||||
}
|
||||
|
@@ -3,13 +3,12 @@ package eventstore
|
||||
import (
|
||||
admin_model "github.com/caos/zitadel/internal/admin/model"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
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/eventsourcing/model"
|
||||
usr_es "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
|
||||
)
|
||||
|
||||
type Setup struct {
|
||||
*org_es.Org
|
||||
*model.Org
|
||||
*usr_es.User
|
||||
}
|
||||
|
||||
@@ -17,7 +16,7 @@ func (s *Setup) AppendEvents(events ...*es_models.Event) error {
|
||||
for _, event := range events {
|
||||
var err error
|
||||
switch event.AggregateType {
|
||||
case org_model.OrgAggregate:
|
||||
case model.OrgAggregate:
|
||||
err = s.Org.AppendEvent(event)
|
||||
case usr_es.UserAggregate:
|
||||
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 {
|
||||
return &admin_model.SetupOrg{
|
||||
Org: org_es.OrgToModel(setup.Org),
|
||||
Org: model.OrgToModel(setup.Org),
|
||||
User: usr_es.UserToModel(setup.User),
|
||||
}
|
||||
}
|
||||
|
@@ -1,12 +1,12 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/org/repository/eventsourcing/model"
|
||||
"time"
|
||||
|
||||
"github.com/caos/logging"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
"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/view"
|
||||
)
|
||||
@@ -37,9 +37,9 @@ func (o *Org) Process(event *es_models.Event) error {
|
||||
org := new(view.OrgView)
|
||||
|
||||
switch event.Type {
|
||||
case org_model.OrgAdded:
|
||||
case model.OrgAdded:
|
||||
org.AppendEvent(event)
|
||||
case org_model.OrgChanged:
|
||||
case model.OrgChanged:
|
||||
err := org.SetData(event)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@@ -125,7 +125,7 @@ func (s *Setup) Execute(ctx context.Context) error {
|
||||
|
||||
err = setUp.setIamProject(ctx)
|
||||
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
|
||||
}
|
||||
|
||||
|
@@ -11,9 +11,9 @@ import (
|
||||
|
||||
type key int
|
||||
|
||||
var (
|
||||
permissionsKey key
|
||||
dataKey key
|
||||
const (
|
||||
permissionsKey key = 1
|
||||
dataKey key = 2
|
||||
)
|
||||
|
||||
type CtxData struct {
|
||||
@@ -36,7 +36,7 @@ type Grant struct {
|
||||
|
||||
type TokenVerifier interface {
|
||||
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)
|
||||
}
|
||||
|
||||
|
@@ -11,21 +11,20 @@ func getUserMethodPermissions(ctx context.Context, t TokenVerifier, requiredPerm
|
||||
if ctxData.IsZero() {
|
||||
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 {
|
||||
return nil, nil, err
|
||||
}
|
||||
permissions := mapGrantsToPermissions(requiredPerm, grants, authConfig)
|
||||
permissions := mapGrantToPermissions(requiredPerm, grant, authConfig)
|
||||
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)
|
||||
for _, grant := range grants {
|
||||
for _, role := range grant.Roles {
|
||||
resolvedPermissions = mapRoleToPerm(requiredPerm, role, authConfig, resolvedPermissions)
|
||||
}
|
||||
for _, role := range grant.Roles {
|
||||
resolvedPermissions = mapRoleToPerm(requiredPerm, role, authConfig, resolvedPermissions)
|
||||
}
|
||||
|
||||
return resolvedPermissions
|
||||
}
|
||||
|
||||
@@ -36,7 +35,7 @@ func mapRoleToPerm(requiredPerm, actualRole string, authConfig *Config, resolved
|
||||
for _, p := range perms {
|
||||
if p == requiredPerm {
|
||||
p = addRoleContextIDToPerm(p, roleContextID)
|
||||
if !existsPerm(resolvedPermissions, p) {
|
||||
if !ExistsPerm(resolvedPermissions, p) {
|
||||
resolvedPermissions = append(resolvedPermissions, p)
|
||||
}
|
||||
}
|
||||
@@ -51,7 +50,7 @@ func addRoleContextIDToPerm(perm, roleContextID string) string {
|
||||
return perm
|
||||
}
|
||||
|
||||
func existsPerm(existing []string, perm string) bool {
|
||||
func ExistsPerm(existing []string, perm string) bool {
|
||||
for _, e := range existing {
|
||||
if e == perm {
|
||||
return true
|
||||
|
@@ -12,15 +12,15 @@ func getTestCtx(userID, orgID string) context.Context {
|
||||
}
|
||||
|
||||
type testVerifier struct {
|
||||
grants []*Grant
|
||||
grant *Grant
|
||||
}
|
||||
|
||||
func (v *testVerifier) VerifyAccessToken(ctx context.Context, token string) (string, string, string, error) {
|
||||
return "userID", "clientID", "agentID", nil
|
||||
}
|
||||
|
||||
func (v *testVerifier) ResolveGrants(ctx context.Context, sub, orgID string) ([]*Grant, error) {
|
||||
return v.grants, nil
|
||||
func (v *testVerifier) ResolveGrant(ctx context.Context) (*Grant, error) {
|
||||
return v.grant, nil
|
||||
}
|
||||
|
||||
func (v *testVerifier) GetProjectIDByClientID(ctx context.Context, clientID string) (string, error) {
|
||||
@@ -57,8 +57,8 @@ func Test_GetUserMethodPermissions(t *testing.T) {
|
||||
name: "Empty Context",
|
||||
args: args{
|
||||
ctx: getTestCtx("", ""),
|
||||
verifier: &testVerifier{grants: []*Grant{&Grant{
|
||||
Roles: []string{"ORG_OWNER"}}}},
|
||||
verifier: &testVerifier{grant: &Grant{
|
||||
Roles: []string{"ORG_OWNER"}}},
|
||||
requiredPerm: "project.read",
|
||||
authConfig: &Config{
|
||||
RolePermissionMappings: []RoleMapping{
|
||||
@@ -81,7 +81,7 @@ func Test_GetUserMethodPermissions(t *testing.T) {
|
||||
name: "No Grants",
|
||||
args: args{
|
||||
ctx: getTestCtx("", ""),
|
||||
verifier: &testVerifier{grants: []*Grant{}},
|
||||
verifier: &testVerifier{grant: &Grant{}},
|
||||
requiredPerm: "project.read",
|
||||
authConfig: &Config{
|
||||
RolePermissionMappings: []RoleMapping{
|
||||
@@ -102,8 +102,8 @@ func Test_GetUserMethodPermissions(t *testing.T) {
|
||||
name: "Get Permissions",
|
||||
args: args{
|
||||
ctx: getTestCtx("userID", "orgID"),
|
||||
verifier: &testVerifier{grants: []*Grant{&Grant{
|
||||
Roles: []string{"ORG_OWNER"}}}},
|
||||
verifier: &testVerifier{grant: &Grant{
|
||||
Roles: []string{"ORG_OWNER"}}},
|
||||
requiredPerm: "project.read",
|
||||
authConfig: &Config{
|
||||
RolePermissionMappings: []RoleMapping{
|
||||
@@ -143,7 +143,7 @@ func Test_GetUserMethodPermissions(t *testing.T) {
|
||||
func Test_MapGrantsToPermissions(t *testing.T) {
|
||||
type args struct {
|
||||
requiredPerm string
|
||||
grants []*Grant
|
||||
grant *Grant
|
||||
authConfig *Config
|
||||
}
|
||||
tests := []struct {
|
||||
@@ -155,8 +155,7 @@ func Test_MapGrantsToPermissions(t *testing.T) {
|
||||
name: "One Role existing perm",
|
||||
args: args{
|
||||
requiredPerm: "project.read",
|
||||
grants: []*Grant{&Grant{
|
||||
Roles: []string{"ORG_OWNER"}}},
|
||||
grant: &Grant{Roles: []string{"ORG_OWNER"}},
|
||||
authConfig: &Config{
|
||||
RolePermissionMappings: []RoleMapping{
|
||||
RoleMapping{
|
||||
@@ -176,8 +175,7 @@ func Test_MapGrantsToPermissions(t *testing.T) {
|
||||
name: "One Role not existing perm",
|
||||
args: args{
|
||||
requiredPerm: "project.write",
|
||||
grants: []*Grant{&Grant{
|
||||
Roles: []string{"ORG_OWNER"}}},
|
||||
grant: &Grant{Roles: []string{"ORG_OWNER"}},
|
||||
authConfig: &Config{
|
||||
RolePermissionMappings: []RoleMapping{
|
||||
RoleMapping{
|
||||
@@ -197,8 +195,7 @@ func Test_MapGrantsToPermissions(t *testing.T) {
|
||||
name: "Multiple Roles one existing",
|
||||
args: args{
|
||||
requiredPerm: "project.read",
|
||||
grants: []*Grant{&Grant{
|
||||
Roles: []string{"ORG_OWNER", "IAM_OWNER"}}},
|
||||
grant: &Grant{Roles: []string{"ORG_OWNER", "IAM_OWNER"}},
|
||||
authConfig: &Config{
|
||||
RolePermissionMappings: []RoleMapping{
|
||||
RoleMapping{
|
||||
@@ -218,8 +215,7 @@ func Test_MapGrantsToPermissions(t *testing.T) {
|
||||
name: "Multiple Roles, global and specific",
|
||||
args: args{
|
||||
requiredPerm: "project.read",
|
||||
grants: []*Grant{&Grant{
|
||||
Roles: []string{"ORG_OWNER", "PROJECT_OWNER:1"}}},
|
||||
grant: &Grant{Roles: []string{"ORG_OWNER", "PROJECT_OWNER:1"}},
|
||||
authConfig: &Config{
|
||||
RolePermissionMappings: []RoleMapping{
|
||||
RoleMapping{
|
||||
@@ -238,7 +234,7 @@ func Test_MapGrantsToPermissions(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
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) {
|
||||
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 {
|
||||
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 {
|
||||
t.Errorf("got wrong result, expecting: %v, actual: %v ", tt.result, result)
|
||||
}
|
||||
|
@@ -7,6 +7,8 @@ const (
|
||||
ContentType = "content-type"
|
||||
Location = "location"
|
||||
Origin = "origin"
|
||||
UserAgent = "user-agent"
|
||||
ForwardedFor = "x-forwarded-for"
|
||||
|
||||
ZitadelOrgID = "x-zitadel-orgid"
|
||||
//TODO: Remove as soon an authentification is implemented
|
||||
|
71
internal/api/http/header.go
Normal file
71
internal/api/http/header.go
Normal 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
|
||||
}
|
68
internal/api/http/user_agent_cookie.go
Normal file
68
internal/api/http/user_agent_cookie.go
Normal 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
|
||||
}
|
@@ -3,23 +3,34 @@ package auth
|
||||
import (
|
||||
"context"
|
||||
"github.com/caos/zitadel/internal/api/auth"
|
||||
authz_repo "github.com/caos/zitadel/internal/authz/repository/eventsourcing"
|
||||
)
|
||||
|
||||
const (
|
||||
authName = "Auth-API"
|
||||
)
|
||||
|
||||
type TokenVerifier struct {
|
||||
authID string
|
||||
authZRepo *authz_repo.EsRepository
|
||||
}
|
||||
|
||||
func Start() (v *TokenVerifier) {
|
||||
return new(TokenVerifier)
|
||||
func Start(authZRepo *authz_repo.EsRepository) (v *TokenVerifier) {
|
||||
return &TokenVerifier{authZRepo: authZRepo}
|
||||
}
|
||||
|
||||
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) {
|
||||
return nil, nil
|
||||
func (v *TokenVerifier) ResolveGrant(ctx context.Context) (*auth.Grant, error) {
|
||||
return v.authZRepo.ResolveGrants(ctx)
|
||||
}
|
||||
|
||||
func (v *TokenVerifier) GetProjectIDByClientID(ctx context.Context, clientID string) (string, error) {
|
||||
return "", nil
|
||||
return v.authZRepo.ProjectIDByClientID(ctx, clientID)
|
||||
}
|
||||
|
@@ -1,4 +0,0 @@
|
||||
package auth
|
||||
|
||||
type Config struct {
|
||||
}
|
12
internal/auth/repository/application.go
Normal file
12
internal/auth/repository/application.go
Normal 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
|
||||
}
|
@@ -9,7 +9,12 @@ import (
|
||||
type AuthRequestRepository interface {
|
||||
CreateAuthRequest(ctx context.Context, request *model.AuthRequest) (*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
|
||||
SelectUser(ctx context.Context, id, userID string) 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
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
@@ -4,23 +4,28 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/caos/logging"
|
||||
|
||||
"github.com/caos/zitadel/internal/auth/repository/eventsourcing/view"
|
||||
"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"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
"github.com/caos/zitadel/internal/id"
|
||||
user_model "github.com/caos/zitadel/internal/user/model"
|
||||
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"
|
||||
)
|
||||
|
||||
type AuthRequestRepo struct {
|
||||
UserEvents *user_event.UserEventstore
|
||||
AuthRequests *cache.AuthRequestCache
|
||||
AuthRequests cache.AuthRequestCache
|
||||
View *view.View
|
||||
|
||||
UserSessionViewProvider userSessionViewProvider
|
||||
UserViewProvider userViewProvider
|
||||
UserEventProvider userEventProvider
|
||||
|
||||
IdGenerator id.Generator
|
||||
|
||||
@@ -38,6 +43,10 @@ type userViewProvider interface {
|
||||
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 {
|
||||
if err := repo.UserEvents.Health(ctx); err != nil {
|
||||
return err
|
||||
@@ -51,6 +60,11 @@ func (repo *AuthRequestRepo) CreateAuthRequest(ctx context.Context, request *mod
|
||||
return nil, err
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
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) {
|
||||
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)
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
steps, err := repo.nextSteps(request)
|
||||
steps, err := repo.nextSteps(ctx, request, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -71,6 +102,10 @@ func (repo *AuthRequestRepo) AuthRequestByID(ctx context.Context, id string) (*m
|
||||
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 {
|
||||
request, err := repo.AuthRequests.GetAuthRequestByID(ctx, id)
|
||||
if err != nil {
|
||||
@@ -80,8 +115,21 @@ func (repo *AuthRequestRepo) CheckUsername(ctx context.Context, id, username str
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
request.UserID = user.ID
|
||||
return repo.AuthRequests.SaveAuthRequest(ctx, request)
|
||||
request.SetUserInfo(user.ID, user.UserName, user.ResourceOwner)
|
||||
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 {
|
||||
@@ -89,8 +137,8 @@ func (repo *AuthRequestRepo) VerifyPassword(ctx context.Context, id, userID, pas
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if request.UserID == userID {
|
||||
return errors.ThrowPreconditionFailed(nil, "EVENT-ds35D", "user id does not match request id ")
|
||||
if request.UserID != userID {
|
||||
return errors.ThrowPreconditionFailed(nil, "EVENT-ds35D", "user id does not match request id")
|
||||
}
|
||||
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))
|
||||
}
|
||||
|
||||
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 {
|
||||
return nil, errors.ThrowInvalidArgument(nil, "EVENT-ds27a", "request must not be nil")
|
||||
}
|
||||
steps := make([]model.NextStep, 0)
|
||||
if !checkLoggedIn && request.Prompt == model.PromptNone {
|
||||
return append(steps, &model.RedirectToCallbackStep{}), nil
|
||||
}
|
||||
if request.UserID == "" {
|
||||
if request.Prompt != model.PromptNone {
|
||||
steps = append(steps, &model.LoginStep{})
|
||||
}
|
||||
steps = append(steps, &model.LoginStep{})
|
||||
if request.Prompt == model.PromptSelectAccount {
|
||||
users, err := repo.usersForUserSelection(request)
|
||||
if err != nil {
|
||||
@@ -124,15 +186,18 @@ func (repo *AuthRequestRepo) nextSteps(request *model.AuthRequest) ([]model.Next
|
||||
}
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
user, err := userByID(repo.UserViewProvider, request.UserID)
|
||||
userSession, err := userSessionByIDs(ctx, repo.UserSessionViewProvider, repo.UserEventProvider, request.AgentID, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user.InitRequired {
|
||||
return append(steps, &model.InitUserStep{PasswordSet: user.PasswordSet}), nil
|
||||
}
|
||||
if !user.PasswordSet {
|
||||
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) {
|
||||
return append(steps, &model.PasswordStep{}), nil
|
||||
}
|
||||
request.PasswordVerified = true
|
||||
request.AuthTime = userSession.PasswordVerification
|
||||
|
||||
if step, ok := repo.mfaChecked(userSession, request, user); !ok {
|
||||
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) {
|
||||
mfaLevel := request.MfaLevel()
|
||||
required := user.MfaMaxSetUp < mfaLevel
|
||||
if required || !repo.mfaSkippedOrSetUp(user) {
|
||||
promptRequired := user.MfaMaxSetUp < mfaLevel
|
||||
if promptRequired || !repo.mfaSkippedOrSetUp(user) {
|
||||
return &model.MfaPromptStep{
|
||||
Required: required,
|
||||
Required: promptRequired,
|
||||
MfaProviders: user.MfaTypesSetupPossible(mfaLevel),
|
||||
}, false
|
||||
}
|
||||
switch mfaLevel {
|
||||
default:
|
||||
fallthrough
|
||||
case model.MfaLevelNotSetUp:
|
||||
if user.MfaMaxSetUp == model.MfaLevelNotSetUp {
|
||||
return nil, true
|
||||
}
|
||||
fallthrough
|
||||
case model.MfaLevelSoftware:
|
||||
if checkVerificationTime(userSession.MfaSoftwareVerification, repo.MfaSoftwareCheckLifeTime) {
|
||||
request.MfasVerified = append(request.MfasVerified, userSession.MfaSoftwareVerificationType)
|
||||
request.AuthTime = userSession.MfaSoftwareVerification
|
||||
return nil, true
|
||||
}
|
||||
fallthrough
|
||||
case model.MfaLevelHardware:
|
||||
if checkVerificationTime(userSession.MfaHardwareVerification, repo.MfaHardwareCheckLifeTime) {
|
||||
request.MfasVerified = append(request.MfasVerified, userSession.MfaHardwareVerificationType)
|
||||
request.AuthTime = userSession.MfaHardwareVerification
|
||||
return nil, true
|
||||
}
|
||||
}
|
||||
@@ -204,7 +280,7 @@ func (repo *AuthRequestRepo) mfaChecked(userSession *user_model.UserSessionView,
|
||||
}
|
||||
|
||||
func (repo *AuthRequestRepo) mfaSkippedOrSetUp(user *user_model.UserView) bool {
|
||||
if user.MfaMaxSetUp >= 0 {
|
||||
if user.MfaMaxSetUp > model.MfaLevelNotSetUp {
|
||||
return true
|
||||
}
|
||||
return checkVerificationTime(user.MfaInitSkipped, repo.MfaInitSkippedLifeTime)
|
||||
@@ -222,18 +298,55 @@ func userSessionsByUserAgentID(provider userSessionViewProvider, agentID string)
|
||||
return view_model.UserSessionsToModel(session), nil
|
||||
}
|
||||
|
||||
func userSessionByIDs(provider userSessionViewProvider, agentID, userID string) (*user_model.UserSessionView, error) {
|
||||
session, err := provider.UserSessionByIDs(agentID, userID)
|
||||
func userSessionByIDs(ctx context.Context, provider userSessionViewProvider, eventProvider userEventProvider, agentID string, user *user_model.UserView) (*user_model.UserSessionView, error) {
|
||||
session, err := provider.UserSessionByIDs(agentID, user.ID)
|
||||
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) {
|
||||
user, err := provider.UserByID(userID)
|
||||
func userByID(ctx context.Context, viewProvider userViewProvider, eventProvider userEventProvider, userID string) (*user_model.UserView, error) {
|
||||
user, err := viewProvider.UserByID(userID)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
@@ -1,6 +1,8 @@
|
||||
package eventstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -10,8 +12,10 @@ import (
|
||||
"github.com/caos/zitadel/internal/auth_request/model"
|
||||
"github.com/caos/zitadel/internal/auth_request/repository/cache"
|
||||
"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_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"
|
||||
)
|
||||
|
||||
@@ -25,6 +29,16 @@ func (m *mockViewNoUserSession) UserSessionsByAgentID(string) ([]*view_model.Use
|
||||
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 {
|
||||
PasswordVerification 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")
|
||||
}
|
||||
|
||||
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 {
|
||||
InitRequired bool
|
||||
PasswordSet bool
|
||||
PasswordChangeRequired bool
|
||||
IsEmailVerified bool
|
||||
@@ -71,6 +104,7 @@ type mockViewUser struct {
|
||||
|
||||
func (m *mockViewUser) UserByID(string) (*view_model.UserView, error) {
|
||||
return &view_model.UserView{
|
||||
InitRequired: m.InitRequired,
|
||||
PasswordSet: m.PasswordSet,
|
||||
PasswordChangeRequired: m.PasswordChangeRequired,
|
||||
IsEmailVerified: m.IsEmailVerified,
|
||||
@@ -87,13 +121,15 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
||||
View *view.View
|
||||
userSessionViewProvider userSessionViewProvider
|
||||
userViewProvider userViewProvider
|
||||
userEventProvider userEventProvider
|
||||
PasswordCheckLifeTime time.Duration
|
||||
MfaInitSkippedLifeTime time.Duration
|
||||
MfaSoftwareCheckLifeTime time.Duration
|
||||
MfaHardwareCheckLifeTime time.Duration
|
||||
}
|
||||
type args struct {
|
||||
request *model.AuthRequest
|
||||
request *model.AuthRequest
|
||||
checkLoggedIn bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -105,22 +141,22 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
||||
{
|
||||
"request nil, error",
|
||||
fields{},
|
||||
args{nil},
|
||||
args{nil, false},
|
||||
nil,
|
||||
errors.IsErrorInvalidArgument,
|
||||
},
|
||||
{
|
||||
"user not set, login step",
|
||||
"prompt none and checkLoggedIn false, callback step",
|
||||
fields{},
|
||||
args{&model.AuthRequest{}},
|
||||
[]model.NextStep{&model.LoginStep{}},
|
||||
args{&model.AuthRequest{Prompt: model.PromptNone}, false},
|
||||
[]model.NextStep{&model.RedirectToCallbackStep{}},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"user not set and prompt none, no step",
|
||||
"user not set, login step",
|
||||
fields{},
|
||||
args{&model.AuthRequest{Prompt: model.PromptNone}},
|
||||
[]model.NextStep{},
|
||||
args{&model.AuthRequest{}, false},
|
||||
[]model.NextStep{&model.LoginStep{}},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
@@ -128,7 +164,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
||||
fields{
|
||||
userSessionViewProvider: &mockViewNoUserSession{},
|
||||
},
|
||||
args{&model.AuthRequest{Prompt: model.PromptSelectAccount}},
|
||||
args{&model.AuthRequest{Prompt: model.PromptSelectAccount}, false},
|
||||
nil,
|
||||
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.LoginStep{},
|
||||
&model.SelectUserStep{
|
||||
@@ -166,31 +203,63 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"usersession not found, not found error",
|
||||
"user not not found, not found error",
|
||||
fields{
|
||||
userSessionViewProvider: &mockViewNoUserSession{},
|
||||
userViewProvider: &mockViewNoUser{},
|
||||
userEventProvider: &mockEventUser{},
|
||||
},
|
||||
args{&model.AuthRequest{UserID: "UserID"}},
|
||||
args{&model.AuthRequest{UserID: "UserID"}, false},
|
||||
nil,
|
||||
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{
|
||||
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,
|
||||
errors.IsNotFound,
|
||||
},
|
||||
{
|
||||
"password not set, init password step",
|
||||
fields{
|
||||
userSessionViewProvider: &mockViewUserSession{},
|
||||
userViewProvider: &mockViewUser{},
|
||||
userEventProvider: &mockEventUser{},
|
||||
},
|
||||
args{&model.AuthRequest{UserID: "UserID"}},
|
||||
args{&model.AuthRequest{UserID: "UserID"}, false},
|
||||
[]model.NextStep{&model.InitPasswordStep{}},
|
||||
nil,
|
||||
},
|
||||
@@ -201,9 +270,10 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
||||
userViewProvider: &mockViewUser{
|
||||
PasswordSet: true,
|
||||
},
|
||||
userEventProvider: &mockEventUser{},
|
||||
PasswordCheckLifeTime: 10 * 24 * time.Hour,
|
||||
},
|
||||
args{&model.AuthRequest{UserID: "UserID"}},
|
||||
args{&model.AuthRequest{UserID: "UserID"}, false},
|
||||
[]model.NextStep{&model.PasswordStep{}},
|
||||
nil,
|
||||
},
|
||||
@@ -218,10 +288,11 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
||||
OTPState: int32(user_model.MFASTATE_READY),
|
||||
MfaMaxSetUp: int32(model.MfaLevelSoftware),
|
||||
},
|
||||
userEventProvider: &mockEventUser{},
|
||||
PasswordCheckLifeTime: 10 * 24 * time.Hour,
|
||||
MfaSoftwareCheckLifeTime: 18 * time.Hour,
|
||||
},
|
||||
args{&model.AuthRequest{UserID: "UserID"}},
|
||||
args{&model.AuthRequest{UserID: "UserID"}, false},
|
||||
[]model.NextStep{&model.MfaVerificationStep{
|
||||
MfaProviders: []model.MfaType{model.MfaTypeOTP},
|
||||
}},
|
||||
@@ -238,11 +309,13 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
||||
PasswordSet: true,
|
||||
PasswordChangeRequired: true,
|
||||
IsEmailVerified: true,
|
||||
MfaMaxSetUp: int32(model.MfaLevelSoftware),
|
||||
},
|
||||
userEventProvider: &mockEventUser{},
|
||||
PasswordCheckLifeTime: 10 * 24 * time.Hour,
|
||||
MfaSoftwareCheckLifeTime: 18 * time.Hour,
|
||||
},
|
||||
args{&model.AuthRequest{UserID: "UserID"}},
|
||||
args{&model.AuthRequest{UserID: "UserID"}, false},
|
||||
[]model.NextStep{&model.ChangePasswordStep{}},
|
||||
nil,
|
||||
},
|
||||
@@ -255,11 +328,13 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
||||
},
|
||||
userViewProvider: &mockViewUser{
|
||||
PasswordSet: true,
|
||||
MfaMaxSetUp: int32(model.MfaLevelSoftware),
|
||||
},
|
||||
userEventProvider: &mockEventUser{},
|
||||
PasswordCheckLifeTime: 10 * 24 * time.Hour,
|
||||
MfaSoftwareCheckLifeTime: 18 * time.Hour,
|
||||
},
|
||||
args{&model.AuthRequest{UserID: "UserID"}},
|
||||
args{&model.AuthRequest{UserID: "UserID"}, false},
|
||||
[]model.NextStep{&model.VerifyEMailStep{}},
|
||||
nil,
|
||||
},
|
||||
@@ -273,11 +348,13 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
||||
userViewProvider: &mockViewUser{
|
||||
PasswordSet: true,
|
||||
PasswordChangeRequired: true,
|
||||
MfaMaxSetUp: int32(model.MfaLevelSoftware),
|
||||
},
|
||||
userEventProvider: &mockEventUser{},
|
||||
PasswordCheckLifeTime: 10 * 24 * time.Hour,
|
||||
MfaSoftwareCheckLifeTime: 18 * time.Hour,
|
||||
},
|
||||
args{&model.AuthRequest{UserID: "UserID"}},
|
||||
args{&model.AuthRequest{UserID: "UserID"}, false},
|
||||
[]model.NextStep{&model.ChangePasswordStep{}, &model.VerifyEMailStep{}},
|
||||
nil,
|
||||
},
|
||||
@@ -291,11 +368,33 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
||||
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"}},
|
||||
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{}},
|
||||
nil,
|
||||
},
|
||||
@@ -308,12 +407,13 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
||||
View: tt.fields.View,
|
||||
UserSessionViewProvider: tt.fields.userSessionViewProvider,
|
||||
UserViewProvider: tt.fields.userViewProvider,
|
||||
UserEventProvider: tt.fields.userEventProvider,
|
||||
PasswordCheckLifeTime: tt.fields.PasswordCheckLifeTime,
|
||||
MfaInitSkippedLifeTime: tt.fields.MfaInitSkippedLifeTime,
|
||||
MfaSoftwareCheckLifeTime: tt.fields.MfaSoftwareCheckLifeTime,
|
||||
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)) {
|
||||
t.Errorf("nextSteps() wrong error = %v", err)
|
||||
return
|
||||
@@ -360,14 +460,31 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
|
||||
args{
|
||||
request: &model.AuthRequest{},
|
||||
user: &user_model.UserView{
|
||||
MfaMaxSetUp: -1,
|
||||
MfaMaxSetUp: model.MfaLevelNotSetUp,
|
||||
},
|
||||
},
|
||||
&model.MfaPromptStep{
|
||||
MfaProviders: []model.MfaType{},
|
||||
MfaProviders: []model.MfaType{
|
||||
model.MfaTypeOTP,
|
||||
},
|
||||
},
|
||||
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",
|
||||
fields{
|
||||
@@ -376,7 +493,8 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
|
||||
args{
|
||||
request: &model.AuthRequest{},
|
||||
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)},
|
||||
},
|
||||
@@ -391,7 +509,8 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
|
||||
args{
|
||||
request: &model.AuthRequest{},
|
||||
user: &user_model.UserView{
|
||||
OTPState: user_model.MFASTATE_READY,
|
||||
MfaMaxSetUp: model.MfaLevelSoftware,
|
||||
OTPState: user_model.MFASTATE_READY,
|
||||
},
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
16
internal/auth/repository/eventsourcing/eventstore/iam.go
Normal file
16
internal/auth/repository/eventsourcing/eventstore/iam.go
Normal 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)
|
||||
}
|
75
internal/auth/repository/eventsourcing/eventstore/key.go
Normal file
75
internal/auth/repository/eventsourcing/eventstore/key.go
Normal 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})
|
||||
}
|
29
internal/auth/repository/eventsourcing/eventstore/org.go
Normal file
29
internal/auth/repository/eventsourcing/eventstore/org.go
Normal 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
|
||||
}
|
@@ -13,8 +13,8 @@ type TokenRepo struct {
|
||||
View *view.View
|
||||
}
|
||||
|
||||
func (repo *TokenRepo) CreateToken(ctx context.Context, agentID, applicationID, userID string, lifetime time.Duration) (*token_model.Token, error) {
|
||||
token, err := repo.View.CreateToken(agentID, applicationID, userID, lifetime)
|
||||
func (repo *TokenRepo) CreateToken(ctx context.Context, agentID, applicationID, userID string, audience, scopes []string, lifetime time.Duration) (*token_model.Token, error) {
|
||||
token, err := repo.View.CreateToken(agentID, applicationID, userID, audience, scopes, lifetime)
|
||||
if err != nil {
|
||||
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) {
|
||||
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
|
||||
}
|
||||
|
@@ -56,10 +56,18 @@ func (repo *UserRepo) ChangeMyEmail(ctx context.Context, email *model.Email) (*m
|
||||
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 {
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -115,6 +140,19 @@ func (repo *UserRepo) RemoveMyMfaOTP(ctx context.Context) error {
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
if obj.AggregateID != auth.GetCtxData(ctx).UserID {
|
||||
return errors.ThrowPermissionDenied(nil, "EVENT-kFi9w", "object does not belong to user")
|
||||
|
158
internal/auth/repository/eventsourcing/eventstore/user_grant.go
Normal file
158
internal/auth/repository/eventsourcing/eventstore/user_grant.go
Normal 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
|
||||
}
|
@@ -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
|
||||
}
|
@@ -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)
|
||||
}
|
@@ -1,9 +1,15 @@
|
||||
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"
|
||||
org_events "github.com/caos/zitadel/internal/org/repository/eventsourcing"
|
||||
proj_event "github.com/caos/zitadel/internal/project/repository/eventsourcing"
|
||||
"time"
|
||||
|
||||
"github.com/caos/zitadel/internal/auth/repository/eventsourcing/view"
|
||||
"github.com/caos/zitadel/internal/config/types"
|
||||
"github.com/caos/zitadel/internal/eventstore/spooler"
|
||||
usr_event "github.com/caos/zitadel/internal/user/repository/eventsourcing"
|
||||
)
|
||||
@@ -11,7 +17,7 @@ import (
|
||||
type Configs map[string]*Config
|
||||
|
||||
type Config struct {
|
||||
MinimumCycleDurationMillisecond int
|
||||
MinimumCycleDuration types.Duration
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
@@ -22,14 +28,28 @@ type handler 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{
|
||||
&User{handler: handler{view, bulkLimit, configs.cycleDuration("User"), errorCount}},
|
||||
&UserSession{handler: handler{view, bulkLimit, configs.cycleDuration("UserSession"), errorCount}, userEvents: repos.UserEvents},
|
||||
&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 {
|
||||
return 1 * time.Second
|
||||
}
|
||||
return time.Duration(c.MinimumCycleDurationMillisecond) * time.Millisecond
|
||||
return c.MinimumCycleDuration.Duration
|
||||
}
|
||||
|
55
internal/auth/repository/eventsourcing/handler/key.go
Normal file
55
internal/auth/repository/eventsourcing/handler/key.go
Normal 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)
|
||||
}
|
64
internal/auth/repository/eventsourcing/handler/org.go
Normal file
64
internal/auth/repository/eventsourcing/handler/org.go
Normal 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)
|
||||
}
|
@@ -54,7 +54,9 @@ func (p *User) Process(event *models.Event) (err error) {
|
||||
es_model.UserUnlocked,
|
||||
es_model.MfaOtpAdded,
|
||||
es_model.MfaOtpVerified,
|
||||
es_model.MfaOtpRemoved:
|
||||
es_model.MfaOtpRemoved,
|
||||
es_model.MfaInitSkipped,
|
||||
es_model.UserPasswordChanged:
|
||||
user, err = p.view.UserByID(event.AggregateID)
|
||||
if err != nil {
|
||||
return err
|
||||
|
344
internal/auth/repository/eventsourcing/handler/user_grant.go
Normal file
344
internal/auth/repository/eventsourcing/handler/user_grant.go
Normal 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)
|
||||
}
|
@@ -45,10 +45,8 @@ func (u *UserSession) Process(event *models.Event) (err error) {
|
||||
switch event.Type {
|
||||
case es_model.UserPasswordCheckSucceeded,
|
||||
es_model.UserPasswordCheckFailed,
|
||||
es_model.UserPasswordChanged,
|
||||
es_model.MfaOtpCheckSucceeded,
|
||||
es_model.MfaOtpCheckFailed,
|
||||
es_model.MfaOtpRemoved:
|
||||
es_model.MfaOtpCheckFailed:
|
||||
eventData, err := view_model.UserSessionFromEvent(event)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -66,14 +64,22 @@ func (u *UserSession) Process(event *models.Event) (err error) {
|
||||
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:
|
||||
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 {
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@@ -2,6 +2,10 @@ package eventsourcing
|
||||
|
||||
import (
|
||||
"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/handler"
|
||||
@@ -10,18 +14,23 @@ import (
|
||||
"github.com/caos/zitadel/internal/auth_request/repository/cache"
|
||||
sd "github.com/caos/zitadel/internal/config/systemdefaults"
|
||||
"github.com/caos/zitadel/internal/config/types"
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
es_int "github.com/caos/zitadel/internal/eventstore"
|
||||
es_spol "github.com/caos/zitadel/internal/eventstore/spooler"
|
||||
"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_proj "github.com/caos/zitadel/internal/project/repository/eventsourcing"
|
||||
es_user "github.com/caos/zitadel/internal/user/repository/eventsourcing"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
SearchLimit uint64
|
||||
Eventstore es_int.Config
|
||||
AuthRequest cache.Config
|
||||
View types.SQL
|
||||
Spooler spooler.SpoolerConfig
|
||||
KeyConfig es_key.KeyConfig
|
||||
}
|
||||
|
||||
type EsRepository struct {
|
||||
@@ -29,9 +38,15 @@ type EsRepository struct {
|
||||
eventstore.UserRepo
|
||||
eventstore.AuthRequestRepo
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -41,7 +56,14 @@ func Start(conf Config, systemDefaults sd.SystemDefaults) (*EsRepository, error)
|
||||
if err != nil {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
@@ -70,8 +92,35 @@ func Start(conf Config, systemDefaults sd.SystemDefaults) (*EsRepository, error)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
repos := handler.EventstoreRepos{UserEvents: user}
|
||||
spool := spooler.StartSpooler(conf.Spooler, es, view, sqlClient, repos)
|
||||
key, err := es_key.StartKey(es, conf.KeyConfig, keyAlgorithm, idGenerator)
|
||||
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{
|
||||
spool,
|
||||
@@ -86,13 +135,41 @@ func Start(conf Config, systemDefaults sd.SystemDefaults) (*EsRepository, error)
|
||||
View: view,
|
||||
UserSessionViewProvider: view,
|
||||
UserViewProvider: view,
|
||||
IdGenerator: id.SonyFlakeGenerator,
|
||||
UserEventProvider: user,
|
||||
IdGenerator: idGenerator,
|
||||
PasswordCheckLifeTime: systemDefaults.VerificationLifetimes.PasswordCheck.Duration,
|
||||
MfaInitSkippedLifeTime: systemDefaults.VerificationLifetimes.MfaInitSkip.Duration,
|
||||
MfaSoftwareCheckLifeTime: systemDefaults.VerificationLifetimes.MfaSoftwareCheck.Duration,
|
||||
MfaHardwareCheckLifeTime: systemDefaults.VerificationLifetimes.MfaHardwareCheck.Duration,
|
||||
},
|
||||
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
|
||||
}
|
||||
|
||||
|
@@ -2,6 +2,7 @@ package spooler
|
||||
|
||||
import (
|
||||
"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/view"
|
||||
@@ -17,12 +18,12 @@ type SpoolerConfig struct {
|
||||
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{
|
||||
Eventstore: es,
|
||||
Locker: &locker{dbClient: sql},
|
||||
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.Start()
|
||||
|
105
internal/auth/repository/eventsourcing/view/application.go
Normal file
105
internal/auth/repository/eventsourcing/view/application.go
Normal 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
|
||||
}
|
72
internal/auth/repository/eventsourcing/view/key.go
Normal file
72
internal/auth/repository/eventsourcing/view/key.go
Normal 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)
|
||||
}
|
43
internal/auth/repository/eventsourcing/view/org.go
Normal file
43
internal/auth/repository/eventsourcing/view/org.go
Normal 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)
|
||||
}
|
@@ -20,17 +20,23 @@ func (v *View) IsTokenValid(tokenID string) (bool, error) {
|
||||
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()
|
||||
token := &model.Token{
|
||||
ID: id,
|
||||
CreationDate: now,
|
||||
UserID: userID,
|
||||
ApplicationID: applicationID,
|
||||
UserAgentID: agentID,
|
||||
Scopes: scopes,
|
||||
Audience: audience,
|
||||
Expiration: now.Add(lifetime),
|
||||
}
|
||||
err := view.PutToken(v.Db, tokenTable, token)
|
||||
if err != nil {
|
||||
if err := view.PutToken(v.Db, tokenTable, token); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return token, nil
|
||||
|
64
internal/auth/repository/eventsourcing/view/user_grant.go
Normal file
64
internal/auth/repository/eventsourcing/view/user_grant.go
Normal 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)
|
||||
}
|
@@ -10,14 +10,14 @@ const (
|
||||
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) {
|
||||
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) {
|
||||
return view.UserSessionsByAgentID(v.Db, userSessionTable, agentID)
|
||||
}
|
||||
|
@@ -4,19 +4,26 @@ import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
"github.com/caos/zitadel/internal/id"
|
||||
)
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &View{
|
||||
Db: gorm,
|
||||
Db: gorm,
|
||||
keyAlgorithm: keyAlgorithm,
|
||||
idGenerator: idGenerator,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
11
internal/auth/repository/iam.go
Normal file
11
internal/auth/repository/iam.go
Normal 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)
|
||||
}
|
14
internal/auth/repository/key.go
Normal file
14
internal/auth/repository/key.go
Normal 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)
|
||||
}
|
@@ -9,4 +9,8 @@ type Repository interface {
|
||||
UserRepository
|
||||
AuthRequestRepository
|
||||
TokenRepository
|
||||
ApplicationRepository
|
||||
KeyRepository
|
||||
UserSessionRepository
|
||||
UserGrantRepository
|
||||
}
|
||||
|
@@ -8,6 +8,7 @@ import (
|
||||
)
|
||||
|
||||
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)
|
||||
TokenByID(ctx context.Context, tokenID string) (*model.Token, error)
|
||||
}
|
||||
|
@@ -11,10 +11,20 @@ type UserRepository interface {
|
||||
|
||||
myUserRepo
|
||||
SkipMfaInit(ctx context.Context, userID string) error
|
||||
|
||||
RequestPasswordReset(ctx context.Context, username 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
|
||||
|
||||
UserByID(ctx context.Context, userID string) (*model.User, error)
|
||||
}
|
||||
|
||||
type myUserRepo interface {
|
||||
@@ -37,6 +47,6 @@ type myUserRepo interface {
|
||||
ChangeMyPassword(ctx context.Context, old, new string) 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
|
||||
}
|
||||
|
12
internal/auth/repository/user_grant.go
Normal file
12
internal/auth/repository/user_grant.go
Normal 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)
|
||||
}
|
10
internal/auth/repository/user_session.go
Normal file
10
internal/auth/repository/user_session.go
Normal 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)
|
||||
}
|
@@ -2,29 +2,36 @@ package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/caos/zitadel/internal/errors"
|
||||
)
|
||||
|
||||
type AuthRequest struct {
|
||||
ID string
|
||||
AgentID string
|
||||
CreationDate time.Time
|
||||
ChangeDate time.Time
|
||||
BrowserInfo *BrowserInfo
|
||||
ApplicationID string
|
||||
CallbackURI string
|
||||
TransferState string
|
||||
Prompt Prompt
|
||||
PossibleLOAs []LevelOfAssurance
|
||||
UiLocales []string
|
||||
LoginHint string
|
||||
PreselectedUserID string
|
||||
MaxAuthAge uint32
|
||||
Request Request
|
||||
ID string
|
||||
AgentID string
|
||||
CreationDate time.Time
|
||||
ChangeDate time.Time
|
||||
BrowserInfo *BrowserInfo
|
||||
ApplicationID string
|
||||
CallbackURI string
|
||||
TransferState string
|
||||
Prompt Prompt
|
||||
PossibleLOAs []LevelOfAssurance
|
||||
UiLocales []string
|
||||
LoginHint string
|
||||
MaxAuthAge uint32
|
||||
Request Request
|
||||
|
||||
levelOfAssurance LevelOfAssurance
|
||||
projectApplicationIDs []string
|
||||
UserID string
|
||||
PossibleSteps []NextStep
|
||||
levelOfAssurance LevelOfAssurance
|
||||
UserID string
|
||||
UserName string
|
||||
UserOrgID string
|
||||
PossibleSteps []NextStep
|
||||
PasswordVerified bool
|
||||
MfasVerified []MfaType
|
||||
Audience []string
|
||||
AuthTime time.Time
|
||||
Code string
|
||||
}
|
||||
|
||||
type Prompt int32
|
||||
@@ -46,22 +53,30 @@ const (
|
||||
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 {
|
||||
return &AuthRequest{
|
||||
ID: id,
|
||||
AgentID: agentID,
|
||||
BrowserInfo: info,
|
||||
ApplicationID: applicationID,
|
||||
CallbackURI: callbackURI,
|
||||
TransferState: transferState,
|
||||
Prompt: prompt,
|
||||
PossibleLOAs: possibleLOAs,
|
||||
UiLocales: uiLocales,
|
||||
LoginHint: loginHint,
|
||||
PreselectedUserID: preselectedUserID,
|
||||
MaxAuthAge: maxAuthAge,
|
||||
Request: request,
|
||||
ID: id,
|
||||
AgentID: agentID,
|
||||
BrowserInfo: info,
|
||||
ApplicationID: applicationID,
|
||||
CallbackURI: callbackURI,
|
||||
TransferState: transferState,
|
||||
Prompt: prompt,
|
||||
PossibleLOAs: possibleLOAs,
|
||||
UiLocales: uiLocales,
|
||||
LoginHint: loginHint,
|
||||
UserID: preselectedUserID,
|
||||
MaxAuthAge: maxAuthAge,
|
||||
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 {
|
||||
return a.ID != "" &&
|
||||
a.AgentID != "" &&
|
||||
@@ -80,3 +95,9 @@ func (a *AuthRequest) WithCurrentInfo(info *BrowserInfo) *AuthRequest {
|
||||
a.BrowserInfo = info
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *AuthRequest) SetUserInfo(userID string, userName string, userOrgID string) {
|
||||
a.UserID = userID
|
||||
a.UserName = userName
|
||||
a.UserOrgID = userOrgID
|
||||
}
|
||||
|
@@ -1,6 +1,12 @@
|
||||
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 {
|
||||
UserAgent string
|
||||
@@ -8,6 +14,14 @@ type BrowserInfo struct {
|
||||
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 {
|
||||
return i.UserAgent != "" &&
|
||||
i.AcceptLanguage != "" &&
|
||||
|
@@ -10,6 +10,7 @@ const (
|
||||
NextStepUnspecified NextStepType = iota
|
||||
NextStepLogin
|
||||
NextStepUserSelection
|
||||
NextStepInitUser
|
||||
NextStepPassword
|
||||
NextStepChangePassword
|
||||
NextStepInitPassword
|
||||
@@ -26,9 +27,7 @@ const (
|
||||
UserSessionStateTerminated
|
||||
)
|
||||
|
||||
type LoginStep struct {
|
||||
NotFound bool
|
||||
}
|
||||
type LoginStep struct{}
|
||||
|
||||
func (s *LoginStep) Type() NextStepType {
|
||||
return NextStepLogin
|
||||
@@ -48,30 +47,33 @@ type UserSelection struct {
|
||||
UserSessionState UserSessionState
|
||||
}
|
||||
|
||||
type PasswordStep struct {
|
||||
FailureCount uint16
|
||||
type InitUserStep struct {
|
||||
PasswordSet bool
|
||||
}
|
||||
|
||||
func (s *InitUserStep) Type() NextStepType {
|
||||
return NextStepInitUser
|
||||
}
|
||||
|
||||
type PasswordStep struct{}
|
||||
|
||||
func (s *PasswordStep) Type() NextStepType {
|
||||
return NextStepPassword
|
||||
}
|
||||
|
||||
type ChangePasswordStep struct {
|
||||
}
|
||||
type ChangePasswordStep struct{}
|
||||
|
||||
func (s *ChangePasswordStep) Type() NextStepType {
|
||||
return NextStepChangePassword
|
||||
}
|
||||
|
||||
type InitPasswordStep struct {
|
||||
}
|
||||
type InitPasswordStep struct{}
|
||||
|
||||
func (s *InitPasswordStep) Type() NextStepType {
|
||||
return NextStepInitPassword
|
||||
}
|
||||
|
||||
type VerifyEMailStep struct {
|
||||
}
|
||||
type VerifyEMailStep struct{}
|
||||
|
||||
func (s *VerifyEMailStep) Type() NextStepType {
|
||||
return NextStepVerifyEmail
|
||||
@@ -87,7 +89,6 @@ func (s *MfaPromptStep) Type() NextStepType {
|
||||
}
|
||||
|
||||
type MfaVerificationStep struct {
|
||||
FailureCount uint16
|
||||
MfaProviders []MfaType
|
||||
}
|
||||
|
||||
@@ -95,8 +96,7 @@ func (s *MfaVerificationStep) Type() NextStepType {
|
||||
return NextStepMfaVerify
|
||||
}
|
||||
|
||||
type RedirectToCallbackStep struct {
|
||||
}
|
||||
type RedirectToCallbackStep struct{}
|
||||
|
||||
func (s *RedirectToCallbackStep) Type() NextStepType {
|
||||
return NextStepRedirectToCallback
|
||||
@@ -111,7 +111,8 @@ const (
|
||||
type MfaLevel int
|
||||
|
||||
const (
|
||||
MfaLevelSoftware MfaLevel = iota
|
||||
MfaLevelNotSetUp MfaLevel = iota
|
||||
MfaLevelSoftware
|
||||
MfaLevelHardware
|
||||
MfaLevelHardwareCertified
|
||||
)
|
||||
|
@@ -7,6 +7,12 @@ type Request interface {
|
||||
|
||||
type AuthRequestType int32
|
||||
|
||||
var (
|
||||
authRequestTypeMapping = map[AuthRequestType]Request{
|
||||
AuthRequestTypeOIDC: &AuthRequestOIDC{},
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
AuthRequestTypeOIDC AuthRequestType = iota
|
||||
AuthRequestTypeSAML
|
||||
|
47
internal/auth_request/repository/cache/cache.go
vendored
47
internal/auth_request/repository/cache/cache.go
vendored
@@ -5,6 +5,7 @@ import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/caos/zitadel/internal/auth_request/model"
|
||||
"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) {
|
||||
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
|
||||
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 errors.Is(err, sql.ErrNoRows) {
|
||||
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")
|
||||
}
|
||||
request := new(model.AuthRequest)
|
||||
err = json.Unmarshal(b, &request)
|
||||
request, err := model.NewAuthRequestFromType(requestType)
|
||||
if err == nil {
|
||||
err = json.Unmarshal(b, request)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, caos_errs.ThrowInternal(err, "CACHE-2wshg", "unable to unmarshal auth request")
|
||||
}
|
||||
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)
|
||||
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 {
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
@@ -1,3 +1,3 @@
|
||||
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
|
||||
|
@@ -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
|
||||
}
|
@@ -1,5 +1,5 @@
|
||||
// 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
|
||||
@@ -11,31 +11,60 @@ import (
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MockRepository is a mock of Repository interface
|
||||
type MockRepository struct {
|
||||
// MockAuthRequestCache is a mock of AuthRequestCache interface
|
||||
type MockAuthRequestCache struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockRepositoryMockRecorder
|
||||
recorder *MockAuthRequestCacheMockRecorder
|
||||
}
|
||||
|
||||
// MockRepositoryMockRecorder is the mock recorder for MockRepository
|
||||
type MockRepositoryMockRecorder struct {
|
||||
mock *MockRepository
|
||||
// MockAuthRequestCacheMockRecorder is the mock recorder for MockAuthRequestCache
|
||||
type MockAuthRequestCacheMockRecorder struct {
|
||||
mock *MockAuthRequestCache
|
||||
}
|
||||
|
||||
// NewMockRepository creates a new mock instance
|
||||
func NewMockRepository(ctrl *gomock.Controller) *MockRepository {
|
||||
mock := &MockRepository{ctrl: ctrl}
|
||||
mock.recorder = &MockRepositoryMockRecorder{mock}
|
||||
// NewMockAuthRequestCache creates a new mock instance
|
||||
func NewMockAuthRequestCache(ctrl *gomock.Controller) *MockAuthRequestCache {
|
||||
mock := &MockAuthRequestCache{ctrl: ctrl}
|
||||
mock.recorder = &MockAuthRequestCacheMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
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()
|
||||
ret := m.ctrl.Call(m, "GetAuthRequestByID", arg0, arg1)
|
||||
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
|
||||
func (mr *MockRepositoryMockRecorder) GetAuthRequestByID(arg0, arg1 interface{}) *gomock.Call {
|
||||
func (mr *MockAuthRequestCacheMockRecorder) GetAuthRequestByID(arg0, arg1 interface{}) *gomock.Call {
|
||||
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
|
||||
func (m *MockRepository) Health(arg0 context.Context) error {
|
||||
func (m *MockAuthRequestCache) Health(arg0 context.Context) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Health", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
@@ -58,22 +87,35 @@ func (m *MockRepository) Health(arg0 context.Context) error {
|
||||
}
|
||||
|
||||
// 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()
|
||||
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
|
||||
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()
|
||||
ret := m.ctrl.Call(m, "SaveAuthRequest", arg0, arg1)
|
||||
ret0, _ := ret[0].(*model.AuthRequest)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// 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()
|
||||
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)
|
||||
}
|
||||
|
@@ -6,9 +6,12 @@ import (
|
||||
"github.com/caos/zitadel/internal/auth_request/model"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
type AuthRequestCache interface {
|
||||
Health(ctx context.Context) 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
16
internal/authz/authz.go
Normal 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)
|
||||
}
|
20
internal/authz/repository/eventsourcing/eventstore/iam.go
Normal file
20
internal/authz/repository/eventsourcing/eventstore/iam.go
Normal 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)
|
||||
}
|
@@ -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
|
||||
}
|
102
internal/authz/repository/eventsourcing/eventstore/user_grant.go
Normal file
102
internal/authz/repository/eventsourcing/eventstore/user_grant.go
Normal 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
|
||||
}
|
@@ -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)
|
||||
}
|
49
internal/authz/repository/eventsourcing/handler/handler.go
Normal file
49
internal/authz/repository/eventsourcing/handler/handler.go
Normal 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
|
||||
}
|
226
internal/authz/repository/eventsourcing/handler/user_grant.go
Normal file
226
internal/authz/repository/eventsourcing/handler/user_grant.go
Normal 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)
|
||||
}
|
98
internal/authz/repository/eventsourcing/repository.go
Normal file
98
internal/authz/repository/eventsourcing/repository.go
Normal 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
|
||||
}
|
19
internal/authz/repository/eventsourcing/spooler/lock.go
Normal file
19
internal/authz/repository/eventsourcing/spooler/lock.go
Normal 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)
|
||||
}
|
127
internal/authz/repository/eventsourcing/spooler/lock_test.go
Normal file
127
internal/authz/repository/eventsourcing/spooler/lock_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
31
internal/authz/repository/eventsourcing/spooler/spooler.go
Normal file
31
internal/authz/repository/eventsourcing/spooler/spooler.go
Normal 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
|
||||
}
|
60
internal/authz/repository/eventsourcing/view/application.go
Normal file
60
internal/authz/repository/eventsourcing/view/application.go
Normal 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)
|
||||
}
|
17
internal/authz/repository/eventsourcing/view/error_event.go
Normal file
17
internal/authz/repository/eventsourcing/view/error_event.go
Normal 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)
|
||||
}
|
17
internal/authz/repository/eventsourcing/view/sequence.go
Normal file
17
internal/authz/repository/eventsourcing/view/sequence.go
Normal 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)
|
||||
}
|
59
internal/authz/repository/eventsourcing/view/token.go
Normal file
59
internal/authz/repository/eventsourcing/view/token.go
Normal 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)
|
||||
}
|
64
internal/authz/repository/eventsourcing/view/user_grant.go
Normal file
64
internal/authz/repository/eventsourcing/view/user_grant.go
Normal 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)
|
||||
}
|
28
internal/authz/repository/eventsourcing/view/view.go
Normal file
28
internal/authz/repository/eventsourcing/view/view.go
Normal 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()
|
||||
}
|
11
internal/authz/repository/iam.go
Normal file
11
internal/authz/repository/iam.go
Normal 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)
|
||||
}
|
11
internal/authz/repository/repository.go
Normal file
11
internal/authz/repository/repository.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
Health(context.Context) error
|
||||
UserGrantRepository
|
||||
IamRepository
|
||||
}
|
10
internal/authz/repository/token_verifier.go
Normal file
10
internal/authz/repository/token_verifier.go
Normal 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)
|
||||
}
|
11
internal/authz/repository/user_grant.go
Normal file
11
internal/authz/repository/user_grant.go
Normal 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)
|
||||
}
|
@@ -1,6 +1,9 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/caos/zitadel/internal/errors"
|
||||
)
|
||||
|
||||
@@ -35,6 +38,23 @@ type CryptoValue struct {
|
||||
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
|
||||
|
||||
func Crypt(value []byte, c Crypto) (*CryptoValue, error) {
|
||||
|
103
internal/crypto/rsa.go
Normal file
103
internal/crypto/rsa.go
Normal 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
27
internal/form/parser.go
Normal 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
128
internal/i18n/i18n.go
Normal 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
|
||||
}
|
@@ -65,7 +65,7 @@ func (i *Iam) AppendEvent(event *es_models.Event) (err error) {
|
||||
i.SetUpDone = true
|
||||
case IamProjectSet,
|
||||
GlobalOrgSet:
|
||||
err = i.setData(event)
|
||||
err = i.SetData(event)
|
||||
case IamMemberAdded:
|
||||
err = i.appendAddMemberEvent(event)
|
||||
case IamMemberChanged:
|
||||
@@ -76,7 +76,7 @@ func (i *Iam) AppendEvent(event *es_models.Event) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
func (i *Iam) setData(event *es_models.Event) error {
|
||||
func (i *Iam) SetData(event *es_models.Event) error {
|
||||
i.ObjectRoot.AppendEvent(event)
|
||||
if err := json.Unmarshal(event.Data, i); err != nil {
|
||||
logging.Log("EVEN-9sie4").WithError(err).Error("could not unmarshal event data")
|
||||
|
@@ -56,7 +56,7 @@ func IamMemberToModel(member *IamMember) *model.IamMember {
|
||||
|
||||
func (iam *Iam) appendAddMemberEvent(event *es_models.Event) error {
|
||||
member := &IamMember{}
|
||||
err := member.setData(event)
|
||||
err := member.SetData(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -67,7 +67,7 @@ func (iam *Iam) appendAddMemberEvent(event *es_models.Event) error {
|
||||
|
||||
func (iam *Iam) appendChangeMemberEvent(event *es_models.Event) error {
|
||||
member := &IamMember{}
|
||||
err := member.setData(event)
|
||||
err := member.SetData(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -79,7 +79,7 @@ func (iam *Iam) appendChangeMemberEvent(event *es_models.Event) error {
|
||||
|
||||
func (iam *Iam) appendRemoveMemberEvent(event *es_models.Event) error {
|
||||
member := &IamMember{}
|
||||
err := member.setData(event)
|
||||
err := member.SetData(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -91,7 +91,7 @@ func (iam *Iam) appendRemoveMemberEvent(event *es_models.Event) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *IamMember) setData(event *es_models.Event) error {
|
||||
func (m *IamMember) SetData(event *es_models.Event) error {
|
||||
m.ObjectRoot.AppendEvent(event)
|
||||
if err := json.Unmarshal(event.Data, m); err != nil {
|
||||
logging.Log("EVEN-e4dkp").WithError(err).Error("could not unmarshal event data")
|
||||
|
46
internal/key/model/key.go
Normal file
46
internal/key/model/key.go
Normal 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
|
||||
}
|
120
internal/key/model/key_view.go
Normal file
120
internal/key/model/key_view.go
Normal 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
|
||||
}
|
84
internal/key/repository/eventsourcing/eventstore.go
Normal file
84
internal/key/repository/eventsourcing/eventstore.go
Normal 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
|
||||
}
|
33
internal/key/repository/eventsourcing/key.go
Normal file
33
internal/key/repository/eventsourcing/key.go
Normal 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)
|
||||
}
|
||||
}
|
90
internal/key/repository/eventsourcing/model/key.go
Normal file
90
internal/key/repository/eventsourcing/model/key.go
Normal 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
|
||||
}
|
9
internal/key/repository/eventsourcing/model/types.go
Normal file
9
internal/key/repository/eventsourcing/model/types.go
Normal 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"
|
||||
)
|
70
internal/key/repository/view/key.go
Normal file
70
internal/key/repository/view/key.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
|
||||
key_model "github.com/caos/zitadel/internal/key/model"
|
||||
"github.com/caos/zitadel/internal/key/repository/view/model"
|
||||
global_model "github.com/caos/zitadel/internal/model"
|
||||
"github.com/caos/zitadel/internal/view"
|
||||
)
|
||||
|
||||
func KeyByIDAndType(db *gorm.DB, table, keyID string, private bool) (*model.KeyView, error) {
|
||||
key := new(model.KeyView)
|
||||
query := view.PrepareGetByQuery(table,
|
||||
model.KeySearchQuery{Key: key_model.KEYSEARCHKEY_ID, Method: global_model.SEARCHMETHOD_EQUALS, Value: keyID},
|
||||
model.KeySearchQuery{Key: key_model.KEYSEARCHKEY_PRIVATE, Method: global_model.SEARCHMETHOD_EQUALS, Value: private},
|
||||
)
|
||||
err := query(db, key)
|
||||
return key, err
|
||||
}
|
||||
|
||||
func GetSigningKey(db *gorm.DB, table string) (*model.KeyView, error) {
|
||||
key := new(model.KeyView)
|
||||
query := view.PrepareGetByQuery(table,
|
||||
model.KeySearchQuery{Key: key_model.KEYSEARCHKEY_PRIVATE, Method: global_model.SEARCHMETHOD_EQUALS, Value: true},
|
||||
model.KeySearchQuery{Key: key_model.KEYSEARCHKEY_USAGE, Method: global_model.SEARCHMETHOD_EQUALS, Value: key_model.KeyUsageSigning},
|
||||
model.KeySearchQuery{Key: key_model.KEYSEARCHKEY_EXPIRY, Method: global_model.SEARCHMETHOD_GREATER_THAN, Value: time.Now().UTC()},
|
||||
)
|
||||
err := query(db, key)
|
||||
return key, err
|
||||
}
|
||||
|
||||
func GetActivePublicKeys(db *gorm.DB, table string) ([]*model.KeyView, error) {
|
||||
keys := make([]*model.KeyView, 0)
|
||||
query := view.PrepareSearchQuery(table,
|
||||
model.KeySearchRequest{
|
||||
Queries: []*key_model.KeySearchQuery{
|
||||
{Key: key_model.KEYSEARCHKEY_PRIVATE, Method: global_model.SEARCHMETHOD_EQUALS, Value: false},
|
||||
{Key: key_model.KEYSEARCHKEY_USAGE, Method: global_model.SEARCHMETHOD_EQUALS, Value: key_model.KeyUsageSigning},
|
||||
{Key: key_model.KEYSEARCHKEY_EXPIRY, Method: global_model.SEARCHMETHOD_GREATER_THAN, Value: time.Now().UTC()},
|
||||
},
|
||||
},
|
||||
)
|
||||
_, err := query(db, &keys)
|
||||
return keys, err
|
||||
}
|
||||
|
||||
func PutKeys(db *gorm.DB, table string, privateKey, publicKey *model.KeyView) error {
|
||||
save := view.PrepareSave(table)
|
||||
err := save(db, privateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return save(db, publicKey)
|
||||
}
|
||||
|
||||
func DeleteKey(db *gorm.DB, table, keyID string, private bool) error {
|
||||
delete := view.PrepareDeleteByKeys(table,
|
||||
view.Key{Key: model.KeySearchKey(key_model.KEYSEARCHKEY_ID), Value: keyID},
|
||||
view.Key{Key: model.KeySearchKey(key_model.KEYSEARCHKEY_PRIVATE), Value: private},
|
||||
)
|
||||
return delete(db)
|
||||
}
|
||||
|
||||
func DeleteKeyPair(db *gorm.DB, table, keyID string) error {
|
||||
delete := view.PrepareDeleteByKey(table, model.KeySearchKey(key_model.KEYSEARCHKEY_ID), keyID)
|
||||
return delete(db)
|
||||
}
|
88
internal/key/repository/view/model/key.go
Normal file
88
internal/key/repository/view/model/key.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/caos/logging"
|
||||
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/eventstore/models"
|
||||
"github.com/caos/zitadel/internal/key/model"
|
||||
es_model "github.com/caos/zitadel/internal/key/repository/eventsourcing/model"
|
||||
)
|
||||
|
||||
const (
|
||||
KeyKeyID = "id"
|
||||
KeyPrivate = "private"
|
||||
KeyUsage = "usage"
|
||||
KeyAlgorithm = "algorithm"
|
||||
KeyExpiry = "expiry"
|
||||
)
|
||||
|
||||
type KeyView struct {
|
||||
ID string `json:"-" gorm:"column:id;primary_key"`
|
||||
Private sql.NullBool `json:"-" gorm:"column:private;primary_key"`
|
||||
Expiry time.Time `json:"-" gorm:"column:expiry"`
|
||||
Algorithm string `json:"-" gorm:"column:algorithm"`
|
||||
Usage int32 `json:"-" gorm:"column:usage"`
|
||||
Key *crypto.CryptoValue `json:"-" gorm:"column:key"`
|
||||
Sequence uint64 `json:"-" gorm:"column:sequence"`
|
||||
}
|
||||
|
||||
func KeysFromPairEvent(event *models.Event) (*KeyView, *KeyView, error) {
|
||||
pair := new(es_model.KeyPair)
|
||||
if err := json.Unmarshal(event.Data, pair); err != nil {
|
||||
logging.Log("MODEL-s3Ga1").WithError(err).Error("could not unmarshal event data")
|
||||
return nil, nil, caos_errs.ThrowInternal(nil, "MODEL-G3haa", "could not unmarshal data")
|
||||
}
|
||||
privateKey := &KeyView{
|
||||
ID: event.AggregateID,
|
||||
Private: sql.NullBool{Bool: true, Valid: true},
|
||||
Expiry: pair.PrivateKey.Expiry,
|
||||
Algorithm: pair.Algorithm,
|
||||
Usage: pair.Usage,
|
||||
Key: pair.PrivateKey.Key,
|
||||
Sequence: pair.Sequence,
|
||||
}
|
||||
publicKey := &KeyView{
|
||||
ID: event.AggregateID,
|
||||
Private: sql.NullBool{Bool: false, Valid: true},
|
||||
Expiry: pair.PublicKey.Expiry,
|
||||
Algorithm: pair.Algorithm,
|
||||
Usage: pair.Usage,
|
||||
Key: pair.PublicKey.Key,
|
||||
Sequence: pair.Sequence,
|
||||
}
|
||||
return privateKey, publicKey, nil
|
||||
}
|
||||
|
||||
func KeyViewsToModel(keys []*KeyView) []*model.KeyView {
|
||||
converted := make([]*model.KeyView, len(keys))
|
||||
for i, key := range keys {
|
||||
converted[i] = KeyViewToModel(key)
|
||||
}
|
||||
return converted
|
||||
}
|
||||
|
||||
func KeyViewToModel(key *KeyView) *model.KeyView {
|
||||
return &model.KeyView{
|
||||
ID: key.ID,
|
||||
Private: key.Private.Bool,
|
||||
Expiry: key.Expiry,
|
||||
Algorithm: key.Algorithm,
|
||||
Usage: model.KeyUsage(key.Usage),
|
||||
Key: key.Key,
|
||||
Sequence: key.Sequence,
|
||||
}
|
||||
}
|
||||
|
||||
func (k *KeyView) setData(event *models.Event) error {
|
||||
if err := json.Unmarshal(event.Data, k); err != nil {
|
||||
logging.Log("MODEL-4ag41").WithError(err).Error("could not unmarshal event data")
|
||||
return caos_errs.ThrowInternal(nil, "MODEL-GFQ31", "could not unmarshal data")
|
||||
}
|
||||
return nil
|
||||
}
|
65
internal/key/repository/view/model/key_query.go
Normal file
65
internal/key/repository/view/model/key_query.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
key_model "github.com/caos/zitadel/internal/key/model"
|
||||
global_model "github.com/caos/zitadel/internal/model"
|
||||
"github.com/caos/zitadel/internal/view"
|
||||
)
|
||||
|
||||
type KeySearchRequest key_model.KeySearchRequest
|
||||
type KeySearchQuery key_model.KeySearchQuery
|
||||
type KeySearchKey key_model.KeySearchKey
|
||||
|
||||
func (req KeySearchRequest) GetLimit() uint64 {
|
||||
return req.Limit
|
||||
}
|
||||
|
||||
func (req KeySearchRequest) GetOffset() uint64 {
|
||||
return req.Offset
|
||||
}
|
||||
|
||||
func (req KeySearchRequest) GetSortingColumn() view.ColumnKey {
|
||||
if req.SortingColumn == key_model.KEYSEARCHKEY_UNSPECIFIED {
|
||||
return nil
|
||||
}
|
||||
return KeySearchKey(req.SortingColumn)
|
||||
}
|
||||
|
||||
func (req KeySearchRequest) GetAsc() bool {
|
||||
return req.Asc
|
||||
}
|
||||
|
||||
func (req KeySearchRequest) GetQueries() []view.SearchQuery {
|
||||
result := make([]view.SearchQuery, len(req.Queries))
|
||||
for i, q := range req.Queries {
|
||||
result[i] = KeySearchQuery{Key: q.Key, Value: q.Value, Method: q.Method}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (req KeySearchQuery) GetKey() view.ColumnKey {
|
||||
return KeySearchKey(req.Key)
|
||||
}
|
||||
|
||||
func (req KeySearchQuery) GetMethod() global_model.SearchMethod {
|
||||
return req.Method
|
||||
}
|
||||
|
||||
func (req KeySearchQuery) GetValue() interface{} {
|
||||
return req.Value
|
||||
}
|
||||
|
||||
func (key KeySearchKey) ToColumnName() string {
|
||||
switch key_model.KeySearchKey(key) {
|
||||
case key_model.KEYSEARCHKEY_ID:
|
||||
return KeyKeyID
|
||||
case key_model.KEYSEARCHKEY_PRIVATE:
|
||||
return KeyPrivate
|
||||
case key_model.KEYSEARCHKEY_USAGE:
|
||||
return KeyUsage
|
||||
case key_model.KEYSEARCHKEY_EXPIRY:
|
||||
return KeyExpiry
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
@@ -1,4 +0,0 @@
|
||||
package login
|
||||
|
||||
type Config struct {
|
||||
}
|
27
internal/login/handler/auth_request.go
Normal file
27
internal/login/handler/auth_request.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/auth_request/model"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const (
|
||||
queryAuthRequestID = "authRequestID"
|
||||
)
|
||||
|
||||
func (l *Login) getAuthRequest(r *http.Request) (*model.AuthRequest, error) {
|
||||
authRequestID := r.FormValue(queryAuthRequestID)
|
||||
if authRequestID == "" {
|
||||
return nil, nil
|
||||
}
|
||||
return l.authRepo.AuthRequestByID(r.Context(), authRequestID)
|
||||
}
|
||||
|
||||
func (l *Login) getAuthRequestAndParseData(r *http.Request, data interface{}) (*model.AuthRequest, error) {
|
||||
authReq, err := l.getAuthRequest(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = l.parser.Parse(r, data)
|
||||
return authReq, err
|
||||
}
|
11
internal/login/handler/callback_handler.go
Normal file
11
internal/login/handler/callback_handler.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/auth_request/model"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (l *Login) redirectToCallback(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest) {
|
||||
callback := l.oidcAuthCallbackURL + authReq.ID
|
||||
http.Redirect(w, r, callback, http.StatusFound)
|
||||
}
|
52
internal/login/handler/change_password_handler.go
Normal file
52
internal/login/handler/change_password_handler.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/auth_request/model"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const (
|
||||
tmplChangePassword = "changepassword"
|
||||
tmplChangePasswordDone = "changepassworddone"
|
||||
)
|
||||
|
||||
type changePasswordData struct {
|
||||
OldPassword string `schema:"old_password"`
|
||||
NewPassword string `schema:"new_password"`
|
||||
}
|
||||
|
||||
func (l *Login) handleChangePassword(w http.ResponseWriter, r *http.Request) {
|
||||
data := new(changePasswordData)
|
||||
authReq, err := l.getAuthRequestAndParseData(r, data)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
err = l.authRepo.ChangePassword(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, data.OldPassword, data.NewPassword)
|
||||
if err != nil {
|
||||
l.renderChangePassword(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
l.renderChangePasswordDone(w, r, authReq)
|
||||
}
|
||||
|
||||
func (l *Login) renderChangePassword(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, err error) {
|
||||
var errType, errMessage string
|
||||
if err != nil {
|
||||
errMessage = err.Error()
|
||||
}
|
||||
data := userData{
|
||||
baseData: l.getBaseData(r, authReq, "Change Password", errType, errMessage),
|
||||
UserName: authReq.UserName,
|
||||
}
|
||||
l.renderer.RenderTemplate(w, r, l.renderer.Templates[tmplChangePassword], data, nil)
|
||||
}
|
||||
|
||||
func (l *Login) renderChangePasswordDone(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest) {
|
||||
var errType, errMessage string
|
||||
data := userData{
|
||||
baseData: l.getBaseData(r, authReq, "Password Change Done", errType, errMessage),
|
||||
UserName: authReq.UserName,
|
||||
}
|
||||
l.renderer.RenderTemplate(w, r, l.renderer.Templates[tmplChangePasswordDone], data, nil)
|
||||
}
|
18
internal/login/handler/health_handler.go
Normal file
18
internal/login/handler/health_handler.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (l *Login) handleHealthz(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("OK"))
|
||||
}
|
||||
|
||||
func (l *Login) handleReadiness(w http.ResponseWriter, r *http.Request) {
|
||||
err := l.authRepo.Health(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, "not ready", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Write([]byte("OK"))
|
||||
}
|
97
internal/login/handler/init_password_handler.go
Normal file
97
internal/login/handler/init_password_handler.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/auth_request/model"
|
||||
"github.com/caos/zitadel/internal/errors"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const (
|
||||
queryInitPWCode = "code"
|
||||
queryInitPWUserID = "userID"
|
||||
|
||||
tmplInitPassword = "initpassword"
|
||||
tmplInitPasswordDone = "initpassworddone"
|
||||
)
|
||||
|
||||
type initPasswordFormData struct {
|
||||
Code string `schema:"code"`
|
||||
Password string `schema:"password"`
|
||||
PasswordConfirm string `schema:"passwordconfirm"`
|
||||
UserID string `schema:"userID"`
|
||||
Resend bool `schema:"resend"`
|
||||
}
|
||||
|
||||
type initPasswordData struct {
|
||||
baseData
|
||||
Code string
|
||||
UserID string
|
||||
}
|
||||
|
||||
func (l *Login) handleInitPassword(w http.ResponseWriter, r *http.Request) {
|
||||
userID := r.FormValue(queryInitPWUserID)
|
||||
code := r.FormValue(queryInitPWCode)
|
||||
l.renderInitPassword(w, r, nil, userID, code, nil)
|
||||
}
|
||||
|
||||
func (l *Login) handleInitPasswordCheck(w http.ResponseWriter, r *http.Request) {
|
||||
data := new(initPasswordFormData)
|
||||
authReq, err := l.getAuthRequestAndParseData(r, data)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
|
||||
if data.Resend {
|
||||
l.resendPasswordSet(w, r, authReq)
|
||||
return
|
||||
}
|
||||
l.checkPWCode(w, r, authReq, data, nil)
|
||||
}
|
||||
|
||||
func (l *Login) checkPWCode(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, data *initPasswordFormData, err error) {
|
||||
if data.Password != data.PasswordConfirm {
|
||||
err := errors.ThrowInvalidArgument(nil, "VIEW-KaGue", "passwords dont match")
|
||||
l.renderInitPassword(w, r, authReq, data.UserID, data.Code, err)
|
||||
return
|
||||
}
|
||||
userOrg := login
|
||||
if authReq != nil {
|
||||
userOrg = authReq.UserOrgID
|
||||
}
|
||||
err = l.authRepo.SetPassword(setContext(r.Context(), userOrg), data.UserID, data.Code, data.Password)
|
||||
if err != nil {
|
||||
l.renderInitPassword(w, r, authReq, data.UserID, "", err)
|
||||
return
|
||||
}
|
||||
l.renderInitPasswordDone(w, r, authReq)
|
||||
}
|
||||
|
||||
func (l *Login) resendPasswordSet(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest) {
|
||||
err := l.authRepo.RequestPasswordReset(r.Context(), authReq.UserName)
|
||||
l.renderInitPassword(w, r, authReq, authReq.UserID, "", err)
|
||||
}
|
||||
|
||||
func (l *Login) renderInitPassword(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, userID, code string, err error) {
|
||||
var errType, errMessage string
|
||||
if err != nil {
|
||||
errMessage = err.Error()
|
||||
}
|
||||
if userID == "" && authReq != nil {
|
||||
userID = authReq.UserID
|
||||
}
|
||||
data := initPasswordData{
|
||||
baseData: l.getBaseData(r, authReq, "Init Password", errType, errMessage),
|
||||
UserID: userID,
|
||||
Code: code,
|
||||
}
|
||||
l.renderer.RenderTemplate(w, r, l.renderer.Templates[tmplInitPassword], data, nil)
|
||||
}
|
||||
|
||||
func (l *Login) renderInitPasswordDone(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest) {
|
||||
data := userData{
|
||||
baseData: l.getBaseData(r, authReq, "Password Init Done", "", ""),
|
||||
UserName: authReq.UserName,
|
||||
}
|
||||
l.renderer.RenderTemplate(w, r, l.renderer.Templates[tmplInitPasswordDone], data, nil)
|
||||
}
|
105
internal/login/handler/init_user_handler.go
Normal file
105
internal/login/handler/init_user_handler.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/auth_request/model"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const (
|
||||
queryInitUserCode = "code"
|
||||
queryInitUserUserID = "userID"
|
||||
|
||||
tmplInitUser = "inituser"
|
||||
tmplInitUserDone = "inituserdone"
|
||||
)
|
||||
|
||||
type initUserFormData struct {
|
||||
Code string `schema:"code"`
|
||||
Password string `schema:"password"`
|
||||
PasswordConfirm string `schema:"passwordconfirm"`
|
||||
UserID string `schema:"userID"`
|
||||
Resend bool `schema:"resend"`
|
||||
}
|
||||
|
||||
type initUserData struct {
|
||||
baseData
|
||||
Code string
|
||||
UserID string
|
||||
}
|
||||
|
||||
func (l *Login) handleInitUser(w http.ResponseWriter, r *http.Request) {
|
||||
userID := r.FormValue(queryInitUserUserID)
|
||||
code := r.FormValue(queryInitUserCode)
|
||||
l.renderInitUser(w, r, nil, userID, code, nil)
|
||||
}
|
||||
|
||||
func (l *Login) handleInitUserCheck(w http.ResponseWriter, r *http.Request) {
|
||||
data := new(initUserFormData)
|
||||
authReq, err := l.getAuthRequestAndParseData(r, data)
|
||||
if err != nil {
|
||||
l.renderError(w, r, nil, err)
|
||||
return
|
||||
}
|
||||
|
||||
if data.Resend {
|
||||
l.resendUserInit(w, r, authReq, data.UserID)
|
||||
return
|
||||
}
|
||||
l.checkUserInitCode(w, r, authReq, data, nil)
|
||||
}
|
||||
|
||||
func (l *Login) checkUserInitCode(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, data *initUserFormData, err error) {
|
||||
if data.Password != data.PasswordConfirm {
|
||||
err := caos_errs.ThrowInvalidArgument(nil, "VIEW-fsdfd", "passwords dont match")
|
||||
l.renderInitUser(w, r, nil, data.UserID, data.Code, err)
|
||||
return
|
||||
}
|
||||
userOrgID := login
|
||||
if authReq != nil {
|
||||
userOrgID = authReq.UserOrgID
|
||||
}
|
||||
err = l.authRepo.VerifyInitCode(setContext(r.Context(), userOrgID), data.UserID, data.Code, data.Password)
|
||||
if err != nil {
|
||||
l.renderInitUser(w, r, nil, data.UserID, "", err)
|
||||
return
|
||||
}
|
||||
l.renderInitUserDone(w, r, nil)
|
||||
}
|
||||
|
||||
func (l *Login) resendUserInit(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, userID string) {
|
||||
userOrgID := login
|
||||
if authReq != nil {
|
||||
userOrgID = authReq.UserOrgID
|
||||
}
|
||||
err := l.authRepo.ResendInitVerificationMail(setContext(r.Context(), userOrgID), userID)
|
||||
l.renderInitUser(w, r, authReq, userID, "", err)
|
||||
}
|
||||
|
||||
func (l *Login) renderInitUser(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, userID, code string, err error) {
|
||||
var errType, errMessage string
|
||||
if err != nil {
|
||||
errMessage = err.Error()
|
||||
}
|
||||
if authReq != nil {
|
||||
userID = authReq.UserID
|
||||
}
|
||||
data := initUserData{
|
||||
baseData: l.getBaseData(r, nil, "Init User", errType, errMessage),
|
||||
UserID: userID,
|
||||
Code: code,
|
||||
}
|
||||
l.renderer.RenderTemplate(w, r, l.renderer.Templates[tmplInitUser], data, nil)
|
||||
}
|
||||
|
||||
func (l *Login) renderInitUserDone(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest) {
|
||||
var errType, errMessage, userName string
|
||||
if authReq != nil {
|
||||
userName = authReq.UserName
|
||||
}
|
||||
data := userData{
|
||||
baseData: l.getBaseData(r, authReq, "User Init Done", errType, errMessage),
|
||||
UserName: userName,
|
||||
}
|
||||
l.renderer.RenderTemplate(w, r, l.renderer.Templates[tmplInitUserDone], data, nil)
|
||||
}
|
92
internal/login/handler/login.go
Normal file
92
internal/login/handler/login.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/caos/logging"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/rakyll/statik/fs"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/caos/zitadel/internal/api/auth"
|
||||
"github.com/caos/zitadel/internal/auth/repository/eventsourcing"
|
||||
"github.com/caos/zitadel/internal/form"
|
||||
|
||||
_ "github.com/caos/zitadel/internal/login/statik"
|
||||
)
|
||||
|
||||
type Login struct {
|
||||
endpoint string
|
||||
router *mux.Router
|
||||
renderer *Renderer
|
||||
parser *form.Parser
|
||||
authRepo *eventsourcing.EsRepository
|
||||
zitadelURL string
|
||||
oidcAuthCallbackURL string
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Port string
|
||||
OidcAuthCallbackURL string
|
||||
ZitadelURL string
|
||||
LanguageCookieName string
|
||||
DefaultLanguage language.Tag
|
||||
}
|
||||
|
||||
const (
|
||||
login = "LOGIN"
|
||||
)
|
||||
|
||||
func StartLogin(ctx context.Context, config Config, authRepo *eventsourcing.EsRepository) {
|
||||
login := &Login{
|
||||
endpoint: config.Port,
|
||||
oidcAuthCallbackURL: config.OidcAuthCallbackURL,
|
||||
zitadelURL: config.ZitadelURL,
|
||||
authRepo: authRepo,
|
||||
}
|
||||
statikFS, err := fs.NewWithNamespace("login")
|
||||
logging.Log("CONFI-7usEW").OnError(err).Panic("unable to start listener")
|
||||
|
||||
login.router = CreateRouter(login, statikFS)
|
||||
login.renderer = CreateRenderer(statikFS, config.LanguageCookieName, config.DefaultLanguage)
|
||||
login.parser = form.NewParser()
|
||||
login.Listen(ctx)
|
||||
}
|
||||
|
||||
func (l *Login) Listen(ctx context.Context) {
|
||||
if l.endpoint == "" {
|
||||
l.endpoint = ":80"
|
||||
} else {
|
||||
l.endpoint = ":" + l.endpoint
|
||||
}
|
||||
|
||||
defer logging.LogWithFields("APP-xUZof", "port", l.endpoint).Info("html is listening")
|
||||
httpListener, err := net.Listen("tcp", l.endpoint)
|
||||
logging.Log("CONFI-W5q2O").OnError(err).Panic("unable to start listener")
|
||||
|
||||
httpServer := &http.Server{
|
||||
Handler: l.router,
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
if err = httpServer.Shutdown(ctx); err != nil {
|
||||
logging.Log("APP-mJKTv").WithError(err)
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
err := httpServer.Serve(httpListener)
|
||||
logging.Log("APP-oSklt").OnError(err).Panic("unable to start listener")
|
||||
}()
|
||||
}
|
||||
|
||||
func setContext(ctx context.Context, resourceOwner string) context.Context {
|
||||
data := auth.CtxData{
|
||||
UserID: login,
|
||||
OrgID: resourceOwner,
|
||||
}
|
||||
return auth.SetCtxData(ctx, data)
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user