Ported system user service to new store
This commit is contained in:
@@ -12,5 +12,13 @@ func (s Store) convertCredentialsFilter(f types.CredentialsFilter) (query squirr
|
||||
query = query.Where(squirrel.Eq{"crd.kind": f.Kind})
|
||||
}
|
||||
|
||||
if f.Credentials != "" {
|
||||
query = query.Where(squirrel.Eq{"crd.credentials": f.Credentials})
|
||||
}
|
||||
|
||||
if f.OwnerID > 0 {
|
||||
query = query.Where(squirrel.Eq{"crd.rel_owner": f.OwnerID})
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
+32
-11
@@ -1,9 +1,12 @@
|
||||
package rdbms
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/Masterminds/squirrel"
|
||||
"github.com/cortezaproject/corteza-server/pkg/rh"
|
||||
"github.com/cortezaproject/corteza-server/store"
|
||||
"github.com/cortezaproject/corteza-server/system/types"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
func (s Store) convertRoleFilter(f types.RoleFilter) (query squirrel.SelectBuilder, err error) {
|
||||
@@ -54,8 +57,13 @@ func (s Store) convertRoleFilter(f types.RoleFilter) (query squirrel.SelectBuild
|
||||
return
|
||||
}
|
||||
|
||||
func (s Store) roleMemberTable() string {
|
||||
return "sys_role_member"
|
||||
func (s Store) RoleMemberTable(aa ...string) string {
|
||||
var alias string
|
||||
if len(aa) > 0 {
|
||||
alias = " AS " + aa[0]
|
||||
}
|
||||
|
||||
return "sys_role_member" + alias
|
||||
}
|
||||
|
||||
//func (s *Store) MembershipsFindByUserID(roleID uint64) (mm []*types.RoleMember, err error) {
|
||||
@@ -69,15 +77,28 @@ func (s Store) roleMemberTable() string {
|
||||
// sql := "SELECT * FROM " + rl.tableMember() + " WHERE rel_role = ?"
|
||||
// return rval, rl.db().Select(&rval, sql, roleID)
|
||||
//}
|
||||
//
|
||||
//func (s *Store) MemberAddByID(roleID, userID uint64) error {
|
||||
// mod := &types.RoleMember{
|
||||
// RoleID: roleID,
|
||||
// UserID: userID,
|
||||
// }
|
||||
// return rl.db().Replace(rl.tableMember(), mod)
|
||||
//}
|
||||
//
|
||||
|
||||
func (s *Store) AddRoleMembersByID(ctx context.Context, roleID uint64, IDs ...uint64) error {
|
||||
if len(IDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
p = store.Payload{"rel_role": roleID, "rel_user": 0}
|
||||
)
|
||||
|
||||
return Tx(ctx, s.db, s.config, nil, func(db *sqlx.Tx) (err error) {
|
||||
for _, p["rel_user"] = range IDs {
|
||||
err = ExecuteSqlizer(ctx, s.DB(), s.Insert(s.RoleMemberTable()).SetMap(p))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
//func (s *Store) MemberRemoveByID(roleID, userID uint64) error {
|
||||
// mod := &types.RoleMember{
|
||||
// RoleID: roleID,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package rdbms
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/Masterminds/squirrel"
|
||||
"github.com/cortezaproject/corteza-server/pkg/permissions"
|
||||
"github.com/cortezaproject/corteza-server/pkg/rh"
|
||||
@@ -79,3 +81,11 @@ func (s Store) convertUserFilter(f types.UserFilter) (query squirrel.SelectBuild
|
||||
query = query.OrderBy(orderBy...)
|
||||
return
|
||||
}
|
||||
|
||||
func (s Store) CountUsers(ctx context.Context, f types.UserFilter) (uint, error) {
|
||||
if q, err := s.convertUserFilter(f); err != nil {
|
||||
return 0, fmt.Errorf("could not count users: %w", err)
|
||||
} else {
|
||||
return Count(ctx, s.db, q)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package tests
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/cortezaproject/corteza-server/pkg/id"
|
||||
"github.com/cortezaproject/corteza-server/system/types"
|
||||
_ "github.com/joho/godotenv/autoload"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -9,13 +11,22 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func testUsers(t *testing.T, s usersStore) {
|
||||
type (
|
||||
usersStoreAdt interface {
|
||||
usersStore
|
||||
CountUsers(ctx context.Context, f types.UserFilter) (uint, error)
|
||||
}
|
||||
)
|
||||
|
||||
func testUsers(t *testing.T, tmp interface{}) {
|
||||
var (
|
||||
ctx = context.Background()
|
||||
req = require.New(t)
|
||||
|
||||
//err error
|
||||
user *types.User
|
||||
|
||||
s = tmp.(usersStoreAdt)
|
||||
)
|
||||
|
||||
t.Run("create", func(t *testing.T) {
|
||||
@@ -154,4 +165,29 @@ func testUsers(t *testing.T, s usersStore) {
|
||||
t.Run("ordered search", func(t *testing.T) {
|
||||
t.Skip("not implemented")
|
||||
})
|
||||
|
||||
t.Run("count", func(t *testing.T) {
|
||||
var (
|
||||
f = types.UserFilter{}
|
||||
c1, c2 uint
|
||||
err error
|
||||
user = &types.User{ID: id.Next(), CreatedAt: time.Now(), Email: fmt.Sprintf("user-crud+%s@crust.test", time.Now().String())}
|
||||
)
|
||||
|
||||
c1, err = s.CountUsers(ctx, f)
|
||||
req.NoError(err)
|
||||
|
||||
req.NoError(s.CreateUser(ctx, user))
|
||||
|
||||
c2, err = s.CountUsers(ctx, f)
|
||||
req.NoError(err)
|
||||
req.Equal(c1+1, c2)
|
||||
|
||||
req.NoError(s.RemoveUserByID(ctx, user.ID))
|
||||
|
||||
c2, err = s.CountUsers(ctx, f)
|
||||
req.NoError(err)
|
||||
req.Equal(c1, c2)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ func Users(app serviceInitializer) *cobra.Command {
|
||||
db = factory.Database.MustGet()
|
||||
|
||||
userRepo = repository.User(ctx, db)
|
||||
authSvc = service.Auth(ctx)
|
||||
authSvc = service.Auth()
|
||||
|
||||
// @todo email validation
|
||||
user = &types.User{Email: args[0]}
|
||||
@@ -135,7 +135,7 @@ func Users(app serviceInitializer) *cobra.Command {
|
||||
return
|
||||
}
|
||||
|
||||
if err = authSvc.SetPassword(user.ID, string(password)); err != nil {
|
||||
if err = authSvc.SetPassword(ctx, user.ID, string(password)); err != nil {
|
||||
cli.HandleError(err)
|
||||
}
|
||||
}
|
||||
@@ -159,7 +159,7 @@ func Users(app serviceInitializer) *cobra.Command {
|
||||
db = factory.Database.MustGet()
|
||||
|
||||
userRepo = repository.User(ctx, db)
|
||||
authSvc = service.Auth(ctx)
|
||||
authSvc = service.Auth()
|
||||
|
||||
user *types.User
|
||||
err error
|
||||
@@ -183,7 +183,7 @@ func Users(app serviceInitializer) *cobra.Command {
|
||||
return
|
||||
}
|
||||
|
||||
if err = authSvc.SetPassword(user.ID, string(password)); err != nil {
|
||||
if err = authSvc.SetPassword(ctx, user.ID, string(password)); err != nil {
|
||||
cli.HandleError(err)
|
||||
}
|
||||
},
|
||||
|
||||
+16
-14
@@ -2,17 +2,15 @@ package rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/titpetric/factory/resputil"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/pkg/auth"
|
||||
"github.com/cortezaproject/corteza-server/pkg/payload"
|
||||
"github.com/cortezaproject/corteza-server/pkg/payload/outgoing"
|
||||
"github.com/cortezaproject/corteza-server/system/rest/request"
|
||||
"github.com/cortezaproject/corteza-server/system/service"
|
||||
"github.com/cortezaproject/corteza-server/system/types"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/titpetric/factory/resputil"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var _ = errors.Wrap
|
||||
@@ -21,7 +19,7 @@ type (
|
||||
Auth struct {
|
||||
tokenEncoder auth.TokenEncoder
|
||||
settings *types.AppSettings
|
||||
authSvc service.AuthService
|
||||
authSvc authUserService
|
||||
}
|
||||
|
||||
authUserResponse struct {
|
||||
@@ -33,6 +31,13 @@ type (
|
||||
*outgoing.User
|
||||
Roles []string `json:"roles"`
|
||||
}
|
||||
|
||||
authUserService interface {
|
||||
Impersonate(ctx context.Context, userID uint64) (*types.User, error)
|
||||
ValidateAuthRequestToken(ctx context.Context, token string) (user *types.User, err error)
|
||||
CanRegister(ctx context.Context) error
|
||||
LoadRoleMemberships(ctx context.Context, user *types.User) error
|
||||
}
|
||||
)
|
||||
|
||||
func (Auth) New() *Auth {
|
||||
@@ -46,7 +51,7 @@ func (Auth) New() *Auth {
|
||||
func (ctrl *Auth) Check(ctx context.Context, r *request.AuthCheck) (interface{}, error) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if identity := auth.GetIdentityFromContext(ctx); identity != nil && identity.Valid() {
|
||||
if user, err := service.DefaultUser.With(ctx).FindByID(identity.Identity()); err == nil {
|
||||
if user, err := service.DefaultUser.FindByID(identity.Identity()); err == nil {
|
||||
var p *authUserResponse
|
||||
|
||||
if p, err = ctrl.makePayload(ctx, user); err != nil {
|
||||
@@ -72,7 +77,7 @@ func (ctrl *Auth) Logout(ctx context.Context, r *request.AuthLogout) (interface{
|
||||
//
|
||||
// This is experimental and internals will most likely change in the future:
|
||||
func (ctrl *Auth) Impersonate(ctx context.Context, r *request.AuthImpersonate) (interface{}, error) {
|
||||
u, err := ctrl.authSvc.With(ctx).Impersonate(r.UserID)
|
||||
u, err := ctrl.authSvc.Impersonate(ctx, r.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -96,7 +101,7 @@ func (ctrl *Auth) Settings(ctx context.Context, r *request.AuthSettings) (interf
|
||||
}
|
||||
)
|
||||
|
||||
if err := ctrl.authSvc.With(ctx).CanRegister(); err != nil {
|
||||
if err := ctrl.authSvc.CanRegister(ctx); err != nil {
|
||||
// f["internalSignUpEnabled"] = false
|
||||
out["signUpDisabled"] = err.Error()
|
||||
}
|
||||
@@ -105,9 +110,7 @@ func (ctrl *Auth) Settings(ctx context.Context, r *request.AuthSettings) (interf
|
||||
}
|
||||
|
||||
func (ctrl *Auth) ExchangeAuthToken(ctx context.Context, r *request.AuthExchangeAuthToken) (interface{}, error) {
|
||||
var svc = ctrl.authSvc.With(ctx)
|
||||
|
||||
user, err := svc.ValidateAuthRequestToken(r.Token)
|
||||
user, err := ctrl.authSvc.ValidateAuthRequestToken(ctx, r.Token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -116,8 +119,7 @@ func (ctrl *Auth) ExchangeAuthToken(ctx context.Context, r *request.AuthExchange
|
||||
}
|
||||
|
||||
func (ctrl *Auth) makePayload(ctx context.Context, user *types.User) (*authUserResponse, error) {
|
||||
var svc = ctrl.authSvc.With(ctx)
|
||||
if err := svc.LoadRoleMemberships(user); err != nil {
|
||||
if err := ctrl.authSvc.LoadRoleMemberships(ctx, user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -23,9 +23,15 @@ import (
|
||||
|
||||
type (
|
||||
ExternalAuth struct {
|
||||
auth service.AuthService
|
||||
auth externalAuthService
|
||||
jwtEncoder auth.TokenEncoder
|
||||
}
|
||||
|
||||
externalAuthService interface {
|
||||
External(ctx context.Context, profile goth.User) (*types.User, error)
|
||||
FrontendRedirectURL() string
|
||||
IssueAuthRequestToken(ctx context.Context, user *types.User) (token string, err error)
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -113,28 +119,24 @@ func (ctrl *ExternalAuth) handleFailedCallback(w http.ResponseWriter, r *http.Re
|
||||
// 2) use `auth.frontend.url.redirect` setting
|
||||
// 3) use current url
|
||||
func (ctrl *ExternalAuth) handleSuccessfulAuth(w http.ResponseWriter, r *http.Request, cred goth.User) {
|
||||
ctrl.log(r.Context(), zap.String("provider", cred.Provider)).Info("external login successful")
|
||||
|
||||
svc := ctrl.auth.With(r.Context())
|
||||
|
||||
var (
|
||||
u *types.User
|
||||
err error
|
||||
)
|
||||
|
||||
// Try to login/sign-up external user
|
||||
if u, err = svc.External(cred); err != nil {
|
||||
resputil.JSON(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
u *types.User
|
||||
err error
|
||||
ctx = r.Context()
|
||||
token string
|
||||
redirUrl *url.URL
|
||||
c *http.Cookie
|
||||
)
|
||||
|
||||
ctrl.log(ctx, zap.String("provider", cred.Provider)).Info("external login successful")
|
||||
|
||||
// Try to login/sign-up external user
|
||||
if u, err = ctrl.auth.External(ctx, cred); err != nil {
|
||||
resputil.JSON(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if c, err = r.Cookie(redirCookieName); err != nil && err != http.ErrNoCookie {
|
||||
ctrl.log(ctx, zap.Error(err)).Warn("error reading cookies")
|
||||
}
|
||||
@@ -151,7 +153,7 @@ func (ctrl *ExternalAuth) handleSuccessfulAuth(w http.ResponseWriter, r *http.Re
|
||||
|
||||
if redirUrl == nil {
|
||||
// Try with frontend redirect URL
|
||||
if fru := svc.FrontendRedirectURL(); fru != "" {
|
||||
if fru := ctrl.auth.FrontendRedirectURL(); fru != "" {
|
||||
redirUrl, err = url.Parse(fru)
|
||||
|
||||
if redirUrl == nil {
|
||||
@@ -178,7 +180,7 @@ func (ctrl *ExternalAuth) handleSuccessfulAuth(w http.ResponseWriter, r *http.Re
|
||||
if u != nil {
|
||||
// Append auth request token to the URL
|
||||
// This token is used by the client and exchanged for JWT
|
||||
if token, err = svc.IssueAuthRequestToken(u); err == nil {
|
||||
if token, err = ctrl.auth.IssueAuthRequestToken(ctx, u); err == nil {
|
||||
q.Set("token", token)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,19 @@ type (
|
||||
|
||||
AuthInternal struct {
|
||||
tokenEncoder auth.TokenEncoder
|
||||
authSvc service.AuthService
|
||||
authSvc authInternalAuthService
|
||||
}
|
||||
|
||||
authInternalAuthService interface {
|
||||
InternalSignUp(ctx context.Context, input *types.User, password string) (*types.User, error)
|
||||
InternalLogin(ctx context.Context, email string, password string) (*types.User, error)
|
||||
SetPassword(ctx context.Context, userID uint64, AuthActionPassword string) error
|
||||
ChangePassword(ctx context.Context, userID uint64, oldPassword, AuthActionPassword string) error
|
||||
LoadRoleMemberships(ctx context.Context, user *types.User) error
|
||||
ValidateEmailConfirmationToken(ctx context.Context, token string) (user *types.User, err error)
|
||||
ExchangePasswordResetToken(ctx context.Context, token string) (user *types.User, exchangedToken string, err error)
|
||||
ValidatePasswordResetToken(ctx context.Context, token string) (user *types.User, err error)
|
||||
SendPasswordResetToken(ctx context.Context, email string) (err error)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -40,18 +52,15 @@ func (AuthInternal) New() *AuthInternal {
|
||||
}
|
||||
|
||||
func (ctrl *AuthInternal) Login(ctx context.Context, r *request.AuthInternalLogin) (interface{}, error) {
|
||||
var svc = ctrl.authSvc.With(ctx)
|
||||
u, err := svc.InternalLogin(r.Email, r.Password)
|
||||
u, err := ctrl.authSvc.InternalLogin(ctx, r.Email, r.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ctrl.authInternalValidUserResponse(svc, u)
|
||||
return ctrl.authInternalValidUserResponse(ctx, u)
|
||||
}
|
||||
|
||||
func (ctrl *AuthInternal) Signup(ctx context.Context, r *request.AuthInternalSignup) (interface{}, error) {
|
||||
var svc = ctrl.authSvc.With(ctx)
|
||||
|
||||
newUser := &types.User{
|
||||
Email: r.Email,
|
||||
Handle: r.Handle,
|
||||
@@ -59,7 +68,7 @@ func (ctrl *AuthInternal) Signup(ctx context.Context, r *request.AuthInternalSig
|
||||
Name: r.Name,
|
||||
}
|
||||
|
||||
u, err := svc.InternalSignUp(newUser, r.Password)
|
||||
u, err := ctrl.authSvc.InternalSignUp(ctx, newUser, r.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -69,20 +78,19 @@ func (ctrl *AuthInternal) Signup(ctx context.Context, r *request.AuthInternalSig
|
||||
return authInternalValidUserResponse{User: &authUserPayload{User: payload.User(u)}}, nil
|
||||
}
|
||||
|
||||
if err = svc.LoadRoleMemberships(u); err != nil {
|
||||
if err = ctrl.authSvc.LoadRoleMemberships(ctx, u); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ctrl.authInternalValidUserResponse(svc, u)
|
||||
return ctrl.authInternalValidUserResponse(ctx, u)
|
||||
}
|
||||
|
||||
func (ctrl *AuthInternal) RequestPasswordReset(ctx context.Context, r *request.AuthInternalRequestPasswordReset) (interface{}, error) {
|
||||
return true, ctrl.authSvc.With(ctx).SendPasswordResetToken(r.Email)
|
||||
return true, ctrl.authSvc.SendPasswordResetToken(ctx, r.Email)
|
||||
}
|
||||
|
||||
func (ctrl *AuthInternal) ExchangePasswordResetToken(ctx context.Context, r *request.AuthInternalExchangePasswordResetToken) (interface{}, error) {
|
||||
var svc = ctrl.authSvc.With(ctx)
|
||||
u, token, err := svc.ExchangePasswordResetToken(r.Token)
|
||||
u, token, err := ctrl.authSvc.ExchangePasswordResetToken(ctx, r.Token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -94,39 +102,36 @@ func (ctrl *AuthInternal) ExchangePasswordResetToken(ctx context.Context, r *req
|
||||
}
|
||||
|
||||
func (ctrl *AuthInternal) ResetPassword(ctx context.Context, r *request.AuthInternalResetPassword) (interface{}, error) {
|
||||
var svc = ctrl.authSvc.With(ctx)
|
||||
var u, err = svc.ValidatePasswordResetToken(r.Token)
|
||||
var u, err = ctrl.authSvc.ValidatePasswordResetToken(ctx, r.Token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = svc.SetPassword(u.ID, r.Password)
|
||||
err = ctrl.authSvc.SetPassword(ctx, u.ID, r.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ctrl.authInternalValidUserResponse(svc, u)
|
||||
return ctrl.authInternalValidUserResponse(ctx, u)
|
||||
}
|
||||
|
||||
func (ctrl *AuthInternal) ConfirmEmail(ctx context.Context, r *request.AuthInternalConfirmEmail) (interface{}, error) {
|
||||
var svc = ctrl.authSvc.With(ctx)
|
||||
var u, err = svc.ValidateEmailConfirmationToken(r.Token)
|
||||
var u, err = ctrl.authSvc.ValidateEmailConfirmationToken(ctx, r.Token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ctrl.authInternalValidUserResponse(svc, u)
|
||||
return ctrl.authInternalValidUserResponse(ctx, u)
|
||||
}
|
||||
|
||||
func (ctrl *AuthInternal) ChangePassword(ctx context.Context, r *request.AuthInternalChangePassword) (interface{}, error) {
|
||||
var svc = ctrl.authSvc.With(ctx)
|
||||
var identity = auth.GetIdentityFromContext(ctx)
|
||||
|
||||
if !identity.Valid() {
|
||||
return nil, errors.New("invalid user (not authenticated)")
|
||||
}
|
||||
|
||||
err := svc.ChangePassword(identity.Identity(), r.OldPassword, r.NewPassword)
|
||||
err := ctrl.authSvc.ChangePassword(ctx, identity.Identity(), r.OldPassword, r.NewPassword)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
@@ -134,8 +139,8 @@ func (ctrl *AuthInternal) ChangePassword(ctx context.Context, r *request.AuthInt
|
||||
}
|
||||
}
|
||||
|
||||
func (ctrl AuthInternal) authInternalValidUserResponse(svc interface{ LoadRoleMemberships(*types.User) error }, u *types.User) (*authInternalValidUserResponse, error) {
|
||||
if err := svc.LoadRoleMemberships(u); err != nil {
|
||||
func (ctrl AuthInternal) authInternalValidUserResponse(ctx context.Context, u *types.User) (*authInternalValidUserResponse, error) {
|
||||
if err := ctrl.authSvc.LoadRoleMemberships(ctx, u); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
+216
-216
@@ -2,7 +2,10 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/cortezaproject/corteza-server/pkg/id"
|
||||
"github.com/cortezaproject/corteza-server/store"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
@@ -23,50 +26,16 @@ import (
|
||||
|
||||
type (
|
||||
auth struct {
|
||||
db db
|
||||
ctx context.Context
|
||||
|
||||
actionlog actionlog.Recorder
|
||||
ac authAccessController
|
||||
eventbus eventDispatcher
|
||||
|
||||
subscription authSubscriptionChecker
|
||||
credentials repository.CredentialsRepository
|
||||
users repository.UserRepository
|
||||
roles repository.RoleRepository
|
||||
store authStore
|
||||
settings *types.AppSettings
|
||||
notifications AuthNotificationService
|
||||
|
||||
providerValidator func(string) error
|
||||
now func() *time.Time
|
||||
}
|
||||
|
||||
AuthService interface {
|
||||
With(ctx context.Context) AuthService
|
||||
|
||||
External(profile goth.User) (*types.User, error)
|
||||
FrontendRedirectURL() string
|
||||
|
||||
InternalSignUp(input *types.User, password string) (*types.User, error)
|
||||
InternalLogin(email string, password string) (*types.User, error)
|
||||
SetPassword(userID uint64, AuthActionPassword string) error
|
||||
ChangePassword(userID uint64, oldPassword, AuthActionPassword string) error
|
||||
Impersonate(userID uint64) (*types.User, error)
|
||||
|
||||
IssueAuthRequestToken(user *types.User) (token string, err error)
|
||||
ValidateAuthRequestToken(token string) (user *types.User, err error)
|
||||
ValidateEmailConfirmationToken(token string) (user *types.User, err error)
|
||||
ExchangePasswordResetToken(token string) (user *types.User, exchangedToken string, err error)
|
||||
ValidatePasswordResetToken(token string) (user *types.User, err error)
|
||||
SendEmailAddressConfirmationToken(email string) (err error)
|
||||
SendPasswordResetToken(email string) (err error)
|
||||
|
||||
CanRegister() error
|
||||
|
||||
LoadRoleMemberships(*types.User) error
|
||||
|
||||
checkPasswordStrength(string) bool
|
||||
changePassword(uint64, string) error
|
||||
}
|
||||
|
||||
authAccessController interface {
|
||||
@@ -76,6 +45,15 @@ type (
|
||||
authSubscriptionChecker interface {
|
||||
CanRegister(uint) error
|
||||
}
|
||||
|
||||
authStore interface {
|
||||
usersStore
|
||||
rolesStore
|
||||
credentialsStore
|
||||
|
||||
AddRoleMembersByID(context.Context, uint64, ...uint64) error
|
||||
CountUsers(context.Context, types.UserFilter) (uint, error)
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -97,8 +75,8 @@ func defaultProviderValidator(provider string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func Auth(ctx context.Context) AuthService {
|
||||
return (&auth{
|
||||
func Auth() *auth {
|
||||
return &auth{
|
||||
eventbus: eventbus.Service(),
|
||||
ac: DefaultAccessControl,
|
||||
subscription: CurrentSubscription,
|
||||
@@ -108,36 +86,6 @@ func Auth(ctx context.Context) AuthService {
|
||||
actionlog: DefaultActionlog,
|
||||
|
||||
providerValidator: defaultProviderValidator,
|
||||
|
||||
now: func() *time.Time {
|
||||
var now = time.Now()
|
||||
return &now
|
||||
},
|
||||
}).With(ctx)
|
||||
}
|
||||
|
||||
// With returns copy of service with new context
|
||||
// obsolete approach, will be removed ASAP
|
||||
func (svc auth) With(ctx context.Context) AuthService {
|
||||
db := repository.DB(ctx)
|
||||
return &auth{
|
||||
db: db,
|
||||
ctx: ctx,
|
||||
|
||||
credentials: repository.Credentials(ctx, db),
|
||||
users: repository.User(ctx, db),
|
||||
roles: repository.Role(ctx, db),
|
||||
|
||||
ac: svc.ac,
|
||||
subscription: svc.subscription,
|
||||
settings: svc.settings,
|
||||
notifications: svc.notifications,
|
||||
eventbus: svc.eventbus,
|
||||
providerValidator: svc.providerValidator,
|
||||
|
||||
actionlog: svc.actionlog,
|
||||
|
||||
now: svc.now,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,7 +104,9 @@ func (svc auth) With(ctx context.Context) AuthService {
|
||||
// 2.2. create user on-the-fly if it does not exist
|
||||
// 2.3. create credentials for that social login
|
||||
//
|
||||
func (svc auth) External(profile goth.User) (u *types.User, err error) {
|
||||
// External login/signup does not:
|
||||
// - validate provider on profile, only uses it for matching credentials
|
||||
func (svc auth) External(ctx context.Context, profile goth.User) (u *types.User, err error) {
|
||||
var (
|
||||
authProvider = &types.AuthProvider{Provider: profile.Provider}
|
||||
|
||||
@@ -180,7 +130,8 @@ func (svc auth) External(profile goth.User) (u *types.User, err error) {
|
||||
return AuthErrProfileWithoutValidEmail(aam)
|
||||
}
|
||||
|
||||
if cc, err := svc.credentials.FindByCredentials(profile.Provider, profile.UserID); err == nil {
|
||||
f := types.CredentialsFilter{Kind: profile.Provider, Credentials: profile.UserID}
|
||||
if cc, _, err := svc.store.SearchCredentials(ctx, f); err == nil {
|
||||
// Credentials found, load user
|
||||
for _, c := range cc {
|
||||
if !c.Valid() {
|
||||
@@ -190,36 +141,37 @@ func (svc auth) External(profile goth.User) (u *types.User, err error) {
|
||||
// Add credentials ID for audit log
|
||||
aam.setCredentials(c)
|
||||
|
||||
if u, err = svc.users.FindByID(c.OwnerID); err != nil {
|
||||
if repository.ErrUserNotFound.Eq(err) {
|
||||
if u, err = svc.store.LookupUserByID(ctx, c.OwnerID); err != nil {
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
// Orphaned credentials (no owner)
|
||||
// try to auto-fix this by removing credentials and recreating user
|
||||
if err = svc.credentials.DeleteByID(c.ID); err != nil {
|
||||
if err = svc.store.RemoveCredentialsByID(ctx, c.ID); err != nil {
|
||||
return err
|
||||
} else {
|
||||
goto findByEmail
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Add user ID for audit log
|
||||
aam.setUser(u)
|
||||
svc.ctx = internalAuth.SetIdentityToContext(svc.ctx, u)
|
||||
ctx = internalAuth.SetIdentityToContext(ctx, u)
|
||||
|
||||
if err = svc.eventbus.WaitFor(svc.ctx, event.AuthBeforeLogin(u, authProvider)); err != nil {
|
||||
if err = svc.eventbus.WaitFor(ctx, event.AuthBeforeLogin(u, authProvider)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if u.Valid() {
|
||||
// Valid user, Bingo!
|
||||
c.LastUsedAt = svc.now()
|
||||
if c, err = svc.credentials.Update(c); err != nil {
|
||||
c.LastUsedAt = nowPtr()
|
||||
if err = svc.store.UpdateCredentials(ctx, c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_ = svc.eventbus.WaitFor(svc.ctx, event.AuthAfterLogin(u, authProvider))
|
||||
return svc.recordAction(svc.ctx, aam, AuthActionUpdateCredentials, nil)
|
||||
_ = svc.eventbus.WaitFor(ctx, event.AuthAfterLogin(u, authProvider))
|
||||
return svc.recordAction(ctx, aam, AuthActionUpdateCredentials, nil)
|
||||
} else {
|
||||
// Scenario: linked to an invalid user
|
||||
if len(cc) > 1 {
|
||||
@@ -248,8 +200,11 @@ func (svc auth) External(profile goth.User) (u *types.User, err error) {
|
||||
setUser(nil)
|
||||
|
||||
// Find user via his email
|
||||
if u, err = svc.users.FindByEmail(profile.Email); repository.ErrUserNotFound.Eq(err) {
|
||||
if u, err = svc.store.LookupUserByEmail(ctx, profile.Email); errors.Is(err, store.ErrNotFound) {
|
||||
// @todo check if it is ok to auto-create a user here
|
||||
if err = svc.CanRegister(ctx); err != nil {
|
||||
return AuthErrSubscription(aam).Wrap(err)
|
||||
}
|
||||
|
||||
// In case we do not have this email, create a new user
|
||||
u = &types.User{
|
||||
@@ -262,31 +217,31 @@ func (svc auth) External(profile goth.User) (u *types.User, err error) {
|
||||
u.Handle = profile.NickName
|
||||
}
|
||||
|
||||
if err = svc.CanRegister(); err != nil {
|
||||
return AuthErrSubscription(aam).Wrap(err)
|
||||
}
|
||||
|
||||
if err = svc.eventbus.WaitFor(svc.ctx, event.AuthBeforeSignup(u, authProvider)); err != nil {
|
||||
if err = svc.eventbus.WaitFor(ctx, event.AuthBeforeSignup(u, authProvider)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if u.Handle == "" {
|
||||
createHandle(svc.users, u)
|
||||
createHandle(ctx, svc.store, u)
|
||||
}
|
||||
|
||||
if u, err = svc.users.Create(u); err != nil {
|
||||
u.ID = id.Next()
|
||||
u.CreatedAt = now()
|
||||
if err = svc.store.CreateUser(ctx, u); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aam.setUser(nil)
|
||||
svc.ctx = internalAuth.SetIdentityToContext(svc.ctx, u)
|
||||
ctx = internalAuth.SetIdentityToContext(ctx, u)
|
||||
|
||||
_ = svc.eventbus.WaitFor(svc.ctx, event.AuthAfterSignup(u, authProvider))
|
||||
_ = svc.eventbus.WaitFor(ctx, event.AuthAfterSignup(u, authProvider))
|
||||
|
||||
svc.recordAction(svc.ctx, aam, AuthActionExternalSignup, nil)
|
||||
if err = svc.recordAction(ctx, aam, AuthActionExternalSignup, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Auto-promote first user
|
||||
if err = svc.autoPromote(u); err != nil {
|
||||
if err = svc.autoPromote(ctx, u); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil {
|
||||
@@ -294,13 +249,13 @@ func (svc auth) External(profile goth.User) (u *types.User, err error) {
|
||||
} else {
|
||||
// User found
|
||||
aam.setUser(u)
|
||||
svc.ctx = internalAuth.SetIdentityToContext(svc.ctx, u)
|
||||
ctx = internalAuth.SetIdentityToContext(ctx, u)
|
||||
|
||||
if err = svc.eventbus.WaitFor(svc.ctx, event.AuthBeforeLogin(u, authProvider)); err != nil {
|
||||
if err = svc.eventbus.WaitFor(ctx, event.AuthBeforeLogin(u, authProvider)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_ = svc.eventbus.WaitFor(svc.ctx, event.AuthAfterLogin(u, authProvider))
|
||||
_ = svc.eventbus.WaitFor(ctx, event.AuthAfterLogin(u, authProvider))
|
||||
|
||||
// If user
|
||||
if !u.Valid() {
|
||||
@@ -311,24 +266,26 @@ func (svc auth) External(profile goth.User) (u *types.User, err error) {
|
||||
// If we got to this point, assume that user is authenticated
|
||||
// but credentials need to be stored
|
||||
c := &types.Credentials{
|
||||
ID: id.Next(),
|
||||
CreatedAt: now(),
|
||||
Kind: profile.Provider,
|
||||
OwnerID: u.ID,
|
||||
Credentials: profile.UserID,
|
||||
LastUsedAt: svc.now(),
|
||||
LastUsedAt: nowPtr(),
|
||||
}
|
||||
|
||||
if c, err = svc.credentials.Create(c); err != nil {
|
||||
if err = svc.store.CreateCredentials(ctx, c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aam.setCredentials(c)
|
||||
svc.recordAction(svc.ctx, aam, AuthActionCreateCredentials, nil)
|
||||
svc.recordAction(ctx, aam, AuthActionCreateCredentials, nil)
|
||||
|
||||
// Owner loaded, carry on.
|
||||
return nil
|
||||
}()
|
||||
|
||||
return u, svc.recordAction(svc.ctx, aam, AuthActionAuthenticate, err)
|
||||
return u, svc.recordAction(ctx, aam, AuthActionAuthenticate, err)
|
||||
}
|
||||
|
||||
// FrontendRedirectURL - a proxy to frontend redirect url setting
|
||||
@@ -341,7 +298,7 @@ func (svc auth) FrontendRedirectURL() string {
|
||||
// Forgiving but strict: valid existing users get notified
|
||||
//
|
||||
// We're accepting the whole user object here and copy all we need to the new user
|
||||
func (svc auth) InternalSignUp(input *types.User, password string) (u *types.User, err error) {
|
||||
func (svc auth) InternalSignUp(ctx context.Context, input *types.User, password string) (u *types.User, err error) {
|
||||
var (
|
||||
authProvider = &types.AuthProvider{Provider: credentialsTypePassword}
|
||||
|
||||
@@ -370,10 +327,13 @@ func (svc auth) InternalSignUp(input *types.User, password string) (u *types.Use
|
||||
}
|
||||
|
||||
var eUser *types.User
|
||||
eUser, err = svc.users.FindByEmail(input.Email)
|
||||
eUser, err = svc.store.LookupUserByEmail(ctx, input.Email)
|
||||
if err == nil && eUser.Valid() {
|
||||
var cc types.CredentialsSet
|
||||
cc, err = svc.credentials.FindByKind(eUser.ID, credentialsTypePassword)
|
||||
var (
|
||||
cc types.CredentialsSet
|
||||
f = types.CredentialsFilter{OwnerID: eUser.ID, Kind: credentialsTypePassword}
|
||||
)
|
||||
cc, _, err = svc.store.SearchCredentials(ctx, f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -382,28 +342,29 @@ func (svc auth) InternalSignUp(input *types.User, password string) (u *types.Use
|
||||
return AuthErrInvalidCredentials(aam)
|
||||
} else {
|
||||
// Update last-used-by timestamp on matching credentials
|
||||
c.LastUsedAt = svc.now()
|
||||
c.LastUsedAt = nowPtr()
|
||||
c.UpdatedAt = nowPtr()
|
||||
aam.setCredentials(c)
|
||||
|
||||
if _, err = svc.credentials.Update(c); err != nil {
|
||||
if err = svc.store.UpdateCredentials(ctx, c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// We're not actually doing sign-up here - user exists,
|
||||
// password is a match, so lets trigger before/after user login events
|
||||
if err = svc.eventbus.WaitFor(svc.ctx, event.AuthBeforeLogin(eUser, authProvider)); err != nil {
|
||||
if err = svc.eventbus.WaitFor(ctx, event.AuthBeforeLogin(eUser, authProvider)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !eUser.EmailConfirmed {
|
||||
err = svc.sendEmailAddressConfirmationToken(eUser)
|
||||
err = svc.sendEmailAddressConfirmationToken(ctx, eUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_ = svc.eventbus.WaitFor(svc.ctx, event.AuthAfterLogin(eUser, authProvider))
|
||||
_ = svc.eventbus.WaitFor(ctx, event.AuthAfterLogin(eUser, authProvider))
|
||||
u = eUser
|
||||
return nil
|
||||
|
||||
@@ -418,15 +379,16 @@ func (svc auth) InternalSignUp(input *types.User, password string) (u *types.Use
|
||||
// }
|
||||
//
|
||||
// return nil,nil
|
||||
} else if !repository.ErrUserNotFound.Eq(err) {
|
||||
} else if !errors.Is(err, store.ErrNotFound) {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = svc.CanRegister(); err != nil {
|
||||
if err = svc.CanRegister(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var nUser = &types.User{
|
||||
ID: id.Next(),
|
||||
Email: input.Email,
|
||||
Name: input.Name,
|
||||
Username: input.Username,
|
||||
@@ -436,54 +398,54 @@ func (svc auth) InternalSignUp(input *types.User, password string) (u *types.Use
|
||||
EmailConfirmed: !svc.settings.Auth.Internal.Signup.EmailConfirmationRequired,
|
||||
}
|
||||
|
||||
if err = svc.eventbus.WaitFor(svc.ctx, event.AuthBeforeSignup(nUser, authProvider)); err != nil {
|
||||
if err = svc.eventbus.WaitFor(ctx, event.AuthBeforeSignup(nUser, authProvider)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if input.Handle == "" {
|
||||
createHandle(svc.users, input)
|
||||
createHandle(ctx, svc.store, input)
|
||||
}
|
||||
|
||||
// Whitelisted user data to copy
|
||||
u, err = svc.users.Create(nUser)
|
||||
err = svc.store.CreateUser(ctx, nUser)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aam.setUser(u)
|
||||
_ = svc.eventbus.WaitFor(svc.ctx, event.AuthAfterSignup(u, authProvider))
|
||||
_ = svc.eventbus.WaitFor(ctx, event.AuthAfterSignup(u, authProvider))
|
||||
|
||||
if err = svc.autoPromote(u); err != nil {
|
||||
if err = svc.autoPromote(ctx, u); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(password) > 0 {
|
||||
err = svc.changePassword(u.ID, password)
|
||||
err = svc.SetPassword(ctx, u.ID, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !u.EmailConfirmed {
|
||||
err = svc.sendEmailAddressConfirmationToken(u)
|
||||
err = svc.sendEmailAddressConfirmationToken(ctx, u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return svc.recordAction(svc.ctx, aam, AuthActionSendEmailConfirmationToken, nil)
|
||||
return svc.recordAction(ctx, aam, AuthActionSendEmailConfirmationToken, nil)
|
||||
}
|
||||
|
||||
return nil
|
||||
}()
|
||||
|
||||
return u, svc.recordAction(svc.ctx, aam, AuthActionInternalSignup, err)
|
||||
return u, svc.recordAction(ctx, aam, AuthActionInternalSignup, err)
|
||||
}
|
||||
|
||||
// InternalLogin verifies username/password combination in the internal credentials table
|
||||
//
|
||||
// Expects plain text password as an input
|
||||
func (svc auth) InternalLogin(email string, password string) (u *types.User, err error) {
|
||||
func (svc auth) InternalLogin(ctx context.Context, email string, password string) (u *types.User, err error) {
|
||||
var (
|
||||
authProvider = &types.AuthProvider{Provider: credentialsTypePassword}
|
||||
|
||||
@@ -511,8 +473,8 @@ func (svc auth) InternalLogin(email string, password string) (u *types.User, err
|
||||
cc types.CredentialsSet
|
||||
)
|
||||
|
||||
u, err = svc.users.FindByEmail(email)
|
||||
if repository.ErrUserNotFound.Eq(err) {
|
||||
u, err = svc.store.LookupUserByEmail(ctx, email)
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
return AuthErrFailedForUnknownUser()
|
||||
}
|
||||
|
||||
@@ -521,9 +483,9 @@ func (svc auth) InternalLogin(email string, password string) (u *types.User, err
|
||||
}
|
||||
|
||||
// Update audit meta with found user
|
||||
svc.ctx = internalAuth.SetIdentityToContext(svc.ctx, u)
|
||||
ctx = internalAuth.SetIdentityToContext(ctx, u)
|
||||
|
||||
cc, err = svc.credentials.FindByKind(u.ID, credentialsTypePassword)
|
||||
cc, _, err = svc.store.SearchCredentials(ctx, types.CredentialsFilter{OwnerID: u.ID, Kind: credentialsTypePassword})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -532,15 +494,16 @@ func (svc auth) InternalLogin(email string, password string) (u *types.User, err
|
||||
return AuthErrInvalidCredentials(aam)
|
||||
} else {
|
||||
// Update last-used-by timestamp on matching credentials
|
||||
c.LastUsedAt = svc.now()
|
||||
c.UpdatedAt = nowPtr()
|
||||
c.LastUsedAt = nowPtr()
|
||||
aam.setCredentials(c)
|
||||
|
||||
if _, err = svc.credentials.Update(c); err != nil {
|
||||
if err = svc.store.UpdateCredentials(ctx, c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err = svc.eventbus.WaitFor(svc.ctx, event.AuthBeforeLogin(u, authProvider)); err != nil {
|
||||
if err = svc.eventbus.WaitFor(ctx, event.AuthBeforeLogin(u, authProvider)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -549,18 +512,18 @@ func (svc auth) InternalLogin(email string, password string) (u *types.User, err
|
||||
}
|
||||
|
||||
if !u.EmailConfirmed {
|
||||
if err = svc.sendEmailAddressConfirmationToken(u); err != nil {
|
||||
if err = svc.sendEmailAddressConfirmationToken(ctx, u); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return AuthErrFailedUnconfirmedEmail()
|
||||
}
|
||||
|
||||
_ = svc.eventbus.WaitFor(svc.ctx, event.AuthAfterLogin(u, authProvider))
|
||||
_ = svc.eventbus.WaitFor(ctx, event.AuthAfterLogin(u, authProvider))
|
||||
return nil
|
||||
}()
|
||||
|
||||
return u, svc.recordAction(svc.ctx, aam, AuthActionAuthenticate, err)
|
||||
return u, svc.recordAction(ctx, aam, AuthActionAuthenticate, err)
|
||||
}
|
||||
|
||||
// checkPassword returns true if given (encrypted) password matches any of the credentials
|
||||
@@ -569,7 +532,9 @@ func (svc auth) checkPassword(password string, cc types.CredentialsSet) bool {
|
||||
}
|
||||
|
||||
// SetPassword sets new password for a user
|
||||
func (svc auth) SetPassword(userID uint64, password string) (err error) {
|
||||
//
|
||||
// This function also records an action
|
||||
func (svc auth) SetPassword(ctx context.Context, userID uint64, password string) (err error) {
|
||||
var (
|
||||
u *types.User
|
||||
|
||||
@@ -584,50 +549,50 @@ func (svc auth) SetPassword(userID uint64, password string) (err error) {
|
||||
return AuthErrInteralLoginDisabledByConfig(aam)
|
||||
}
|
||||
|
||||
if !svc.checkPasswordStrength(password) {
|
||||
if !svc.CheckPasswordStrength(password) {
|
||||
return AuthErrPasswordNotSecure(aam)
|
||||
}
|
||||
|
||||
u, err = svc.users.FindByID(userID)
|
||||
if repository.ErrUserNotFound.Eq(err) {
|
||||
u, err = svc.store.LookupUserByID(ctx, userID)
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
return AuthErrPasswordChangeFailedForUnknownUser(aam)
|
||||
}
|
||||
|
||||
if err != svc.changePassword(userID, password) {
|
||||
if err != svc.SetPasswordCredentials(ctx, userID, password) {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}()
|
||||
|
||||
return svc.recordAction(svc.ctx, aam, AuthActionChangePassword, err)
|
||||
return svc.recordAction(ctx, aam, AuthActionChangePassword, err)
|
||||
}
|
||||
|
||||
// Impersonate verifies if user can impersonate another user and returns that user
|
||||
//
|
||||
// For now, it's the caller's responsibility to generate the auth token
|
||||
func (svc auth) Impersonate(userID uint64) (u *types.User, err error) {
|
||||
func (svc auth) Impersonate(ctx context.Context, userID uint64) (u *types.User, err error) {
|
||||
var (
|
||||
aam = &authActionProps{user: u}
|
||||
)
|
||||
|
||||
err = func() error {
|
||||
if u, err = svc.users.FindByID(userID); err != nil {
|
||||
if u, err = svc.store.LookupUserByID(ctx, userID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !svc.ac.CanImpersonateUser(svc.ctx, u) {
|
||||
if !svc.ac.CanImpersonateUser(ctx, u) {
|
||||
return AuthErrNotAllowedToImpersonate()
|
||||
}
|
||||
|
||||
return err
|
||||
}()
|
||||
|
||||
return u, svc.recordAction(svc.ctx, aam, AuthActionImpersonate, err)
|
||||
return u, svc.recordAction(ctx, aam, AuthActionImpersonate, err)
|
||||
}
|
||||
|
||||
// ChangePassword validates old password and changes it with new
|
||||
func (svc auth) ChangePassword(userID uint64, oldPassword, AuthActionPassword string) (err error) {
|
||||
func (svc auth) ChangePassword(ctx context.Context, userID uint64, oldPassword, AuthActionPassword string) (err error) {
|
||||
var (
|
||||
u *types.User
|
||||
cc types.CredentialsSet
|
||||
@@ -647,16 +612,16 @@ func (svc auth) ChangePassword(userID uint64, oldPassword, AuthActionPassword st
|
||||
return AuthErrPasswordNotSecure(aam)
|
||||
}
|
||||
|
||||
if !svc.checkPasswordStrength(AuthActionPassword) {
|
||||
if !svc.CheckPasswordStrength(AuthActionPassword) {
|
||||
return AuthErrPasswordNotSecure(aam)
|
||||
}
|
||||
|
||||
u, err = svc.users.FindByID(userID)
|
||||
if repository.ErrUserNotFound.Eq(err) {
|
||||
u, err = svc.store.LookupUserByID(ctx, userID)
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
return AuthErrPasswordChangeFailedForUnknownUser(aam)
|
||||
}
|
||||
|
||||
cc, err = svc.credentials.FindByKind(userID, credentialsTypePassword)
|
||||
cc, _, err = svc.store.SearchCredentials(ctx, types.CredentialsFilter{Kind: credentialsTypePassword, OwnerID: userID})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -665,21 +630,21 @@ func (svc auth) ChangePassword(userID uint64, oldPassword, AuthActionPassword st
|
||||
return AuthErrPasswodResetFailedOldPasswordCheckFailed(aam)
|
||||
}
|
||||
|
||||
if err != svc.changePassword(userID, AuthActionPassword) {
|
||||
if err != svc.SetPasswordCredentials(ctx, userID, AuthActionPassword) {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}()
|
||||
|
||||
return svc.recordAction(svc.ctx, aam, AuthActionChangePassword, err)
|
||||
return svc.recordAction(ctx, aam, AuthActionChangePassword, err)
|
||||
}
|
||||
|
||||
func (svc auth) hashPassword(password string) (hash []byte, err error) {
|
||||
return bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
}
|
||||
|
||||
func (svc auth) checkPasswordStrength(password string) bool {
|
||||
func (svc auth) CheckPasswordStrength(password string) bool {
|
||||
if len(password) <= 4 {
|
||||
return false
|
||||
}
|
||||
@@ -687,35 +652,57 @@ func (svc auth) checkPasswordStrength(password string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// ChangePassword (soft) deletes old password entry and creates a new one
|
||||
// SetPasswordCredentials (soft) deletes old password entry and creates a new entry with new password on every change
|
||||
//
|
||||
// Expects hashed password as an input
|
||||
func (svc auth) changePassword(userID uint64, password string) (err error) {
|
||||
var hash []byte
|
||||
// This way we can implement more strict password-change policies in the future
|
||||
//
|
||||
// This method is used by auth and user procedures to unify password hashing and updating
|
||||
// credentials
|
||||
func (svc auth) SetPasswordCredentials(ctx context.Context, userID uint64, password string) (err error) {
|
||||
var (
|
||||
hash []byte
|
||||
cc types.CredentialsSet
|
||||
f = types.CredentialsFilter{Kind: credentialsTypePassword, OwnerID: userID}
|
||||
)
|
||||
|
||||
if hash, err = svc.hashPassword(password); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = svc.credentials.DeleteByKind(userID, credentialsTypePassword); err != nil {
|
||||
if cc, _, err = svc.store.SearchCredentials(ctx, f); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Mark all credentials as deleted
|
||||
_ = cc.Walk(func(c *types.Credentials) error {
|
||||
c.DeletedAt = nowPtr()
|
||||
return nil
|
||||
})
|
||||
|
||||
// Do a partial update and soft-delete all
|
||||
if err = svc.store.PartialUpdateCredentials(ctx, []string{"deleted_at"}, cc...); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = svc.credentials.Create(&types.Credentials{
|
||||
// Add new credentials with new password
|
||||
c := &types.Credentials{
|
||||
ID: id.Next(),
|
||||
CreatedAt: now(),
|
||||
OwnerID: userID,
|
||||
Kind: credentialsTypePassword,
|
||||
Credentials: string(hash),
|
||||
})
|
||||
}
|
||||
|
||||
return err
|
||||
return svc.store.CreateCredentials(ctx, c)
|
||||
}
|
||||
|
||||
// IssueAuthRequestToken returns token that can be used for authentication
|
||||
func (svc auth) IssueAuthRequestToken(user *types.User) (token string, err error) {
|
||||
return svc.createUserToken(user, credentialsTypeAuthToken)
|
||||
func (svc auth) IssueAuthRequestToken(ctx context.Context, user *types.User) (token string, err error) {
|
||||
return svc.createUserToken(ctx, user, credentialsTypeAuthToken)
|
||||
}
|
||||
|
||||
// ValidateAuthRequestToken returns user that requested auth token
|
||||
func (svc auth) ValidateAuthRequestToken(token string) (u *types.User, err error) {
|
||||
func (svc auth) ValidateAuthRequestToken(ctx context.Context, token string) (u *types.User, err error) {
|
||||
var (
|
||||
aam = &authActionProps{
|
||||
credentials: &types.Credentials{Kind: credentialsTypeAuthToken},
|
||||
@@ -723,29 +710,29 @@ func (svc auth) ValidateAuthRequestToken(token string) (u *types.User, err error
|
||||
)
|
||||
|
||||
err = func() error {
|
||||
u, err = svc.loadUserFromToken(token, credentialsTypeAuthToken)
|
||||
u, err = svc.loadUserFromToken(ctx, token, credentialsTypeAuthToken)
|
||||
if err != nil && u != nil {
|
||||
aam.setUser(u)
|
||||
svc.ctx = internalAuth.SetIdentityToContext(svc.ctx, u)
|
||||
ctx = internalAuth.SetIdentityToContext(ctx, u)
|
||||
}
|
||||
return err
|
||||
}()
|
||||
|
||||
return u, svc.recordAction(svc.ctx, aam, AuthActionValidateToken, err)
|
||||
return u, svc.recordAction(ctx, aam, AuthActionValidateToken, err)
|
||||
}
|
||||
|
||||
// ValidateEmailConfirmationToken issues a validation token that can be used for
|
||||
func (svc auth) ValidateEmailConfirmationToken(token string) (user *types.User, err error) {
|
||||
return svc.loadFromTokenAndConfirmEmail(token, credentialsTypeEmailAuthToken)
|
||||
func (svc auth) ValidateEmailConfirmationToken(ctx context.Context, token string) (user *types.User, err error) {
|
||||
return svc.loadFromTokenAndConfirmEmail(ctx, token, credentialsTypeEmailAuthToken)
|
||||
}
|
||||
|
||||
// ValidatePasswordResetToken validates password reset token
|
||||
func (svc auth) ValidatePasswordResetToken(token string) (user *types.User, err error) {
|
||||
return svc.loadFromTokenAndConfirmEmail(token, credentialsTypeEmailAuthToken)
|
||||
func (svc auth) ValidatePasswordResetToken(ctx context.Context, token string) (user *types.User, err error) {
|
||||
return svc.loadFromTokenAndConfirmEmail(ctx, token, credentialsTypeEmailAuthToken)
|
||||
}
|
||||
|
||||
// loadFromTokenAndConfirmEmail loads token, confirms user's
|
||||
func (svc auth) loadFromTokenAndConfirmEmail(token, tokenType string) (u *types.User, err error) {
|
||||
func (svc auth) loadFromTokenAndConfirmEmail(ctx context.Context, token, tokenType string) (u *types.User, err error) {
|
||||
var (
|
||||
aam = &authActionProps{
|
||||
user: u,
|
||||
@@ -758,31 +745,32 @@ func (svc auth) loadFromTokenAndConfirmEmail(token, tokenType string) (u *types.
|
||||
return AuthErrInternalSignupDisabledByConfig(aam)
|
||||
}
|
||||
|
||||
u, err = svc.loadUserFromToken(token, tokenType)
|
||||
u, err = svc.loadUserFromToken(ctx, token, tokenType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aam.setUser(u)
|
||||
svc.ctx = internalAuth.SetIdentityToContext(svc.ctx, u)
|
||||
ctx = internalAuth.SetIdentityToContext(ctx, u)
|
||||
|
||||
if u.EmailConfirmed {
|
||||
return nil
|
||||
}
|
||||
|
||||
u.EmailConfirmed = true
|
||||
if u, err = svc.users.Update(u); err != nil {
|
||||
u.UpdatedAt = nowPtr()
|
||||
if err = svc.store.UpdateUser(ctx, u); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}()
|
||||
|
||||
return u, svc.recordAction(svc.ctx, aam, AuthActionConfirmEmail, err)
|
||||
return u, svc.recordAction(ctx, aam, AuthActionConfirmEmail, err)
|
||||
}
|
||||
|
||||
// ExchangePasswordResetToken exchanges reset password token for a new one and returns it with user info
|
||||
func (svc auth) ExchangePasswordResetToken(token string) (u *types.User, t string, err error) {
|
||||
func (svc auth) ExchangePasswordResetToken(ctx context.Context, token string) (u *types.User, t string, err error) {
|
||||
var (
|
||||
aam = &authActionProps{
|
||||
user: u,
|
||||
@@ -795,15 +783,15 @@ func (svc auth) ExchangePasswordResetToken(token string) (u *types.User, t strin
|
||||
return AuthErrPasswordResetDisabledByConfig(aam)
|
||||
}
|
||||
|
||||
u, err = svc.loadUserFromToken(token, credentialsTypeResetPasswordToken)
|
||||
u, err = svc.loadUserFromToken(ctx, token, credentialsTypeResetPasswordToken)
|
||||
if err != nil {
|
||||
return AuthErrInvalidToken(aam).Wrap(err)
|
||||
}
|
||||
|
||||
aam.setUser(u)
|
||||
svc.ctx = internalAuth.SetIdentityToContext(svc.ctx, u)
|
||||
ctx = internalAuth.SetIdentityToContext(ctx, u)
|
||||
|
||||
t, err = svc.createUserToken(u, credentialsTypeResetPasswordTokenExchanged)
|
||||
t, err = svc.createUserToken(ctx, u, credentialsTypeResetPasswordTokenExchanged)
|
||||
if err != nil {
|
||||
u = nil
|
||||
t = ""
|
||||
@@ -813,11 +801,11 @@ func (svc auth) ExchangePasswordResetToken(token string) (u *types.User, t strin
|
||||
return nil
|
||||
}()
|
||||
|
||||
return u, t, svc.recordAction(svc.ctx, aam, AuthActionExchangePasswordResetToken, err)
|
||||
return u, t, svc.recordAction(ctx, aam, AuthActionExchangePasswordResetToken, err)
|
||||
}
|
||||
|
||||
// SendEmailAddressConfirmationToken sends email with email address confirmation token
|
||||
func (svc auth) SendEmailAddressConfirmationToken(email string) (err error) {
|
||||
func (svc auth) SendEmailAddressConfirmationToken(ctx context.Context, email string) (err error) {
|
||||
var (
|
||||
aam = &authActionProps{
|
||||
email: email,
|
||||
@@ -829,18 +817,18 @@ func (svc auth) SendEmailAddressConfirmationToken(email string) (err error) {
|
||||
return AuthErrPasswordResetDisabledByConfig(aam)
|
||||
}
|
||||
|
||||
u, err := svc.users.FindByEmail(email)
|
||||
u, err := svc.store.LookupUserByEmail(ctx, email)
|
||||
if err != nil {
|
||||
return AuthErrInvalidToken(aam)
|
||||
}
|
||||
|
||||
return svc.sendEmailAddressConfirmationToken(u)
|
||||
return svc.sendEmailAddressConfirmationToken(ctx, u)
|
||||
}()
|
||||
|
||||
return svc.recordAction(svc.ctx, aam, AuthActionSendEmailConfirmationToken, err)
|
||||
return svc.recordAction(ctx, aam, AuthActionSendEmailConfirmationToken, err)
|
||||
}
|
||||
|
||||
func (svc auth) sendEmailAddressConfirmationToken(u *types.User) (err error) {
|
||||
func (svc auth) sendEmailAddressConfirmationToken(ctx context.Context, u *types.User) (err error) {
|
||||
var (
|
||||
notificationLang = "en"
|
||||
token string
|
||||
@@ -851,19 +839,19 @@ func (svc auth) sendEmailAddressConfirmationToken(u *types.User) (err error) {
|
||||
}
|
||||
)
|
||||
|
||||
if token, err = svc.createUserToken(u, credentialsTypeEmailAuthToken); err != nil {
|
||||
if token, err = svc.createUserToken(ctx, u, credentialsTypeEmailAuthToken); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = svc.notifications.EmailConfirmation(svc.ctx, notificationLang, u.Email, token); err != nil {
|
||||
if err = svc.notifications.EmailConfirmation(ctx, notificationLang, u.Email, token); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return svc.recordAction(svc.ctx, aam, AuthActionSendEmailConfirmationToken, err)
|
||||
return svc.recordAction(ctx, aam, AuthActionSendEmailConfirmationToken, err)
|
||||
}
|
||||
|
||||
// SendPasswordResetToken sends password reset token to email
|
||||
func (svc auth) SendPasswordResetToken(email string) (err error) {
|
||||
func (svc auth) SendPasswordResetToken(ctx context.Context, email string) (err error) {
|
||||
var (
|
||||
u *types.User
|
||||
|
||||
@@ -878,48 +866,53 @@ func (svc auth) SendPasswordResetToken(email string) (err error) {
|
||||
return AuthErrPasswordResetDisabledByConfig(aam)
|
||||
}
|
||||
|
||||
if u, err = svc.users.FindByEmail(email); err != nil {
|
||||
if u, err = svc.store.LookupUserByEmail(ctx, email); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
svc.ctx = internalAuth.SetIdentityToContext(svc.ctx, u)
|
||||
ctx = internalAuth.SetIdentityToContext(ctx, u)
|
||||
|
||||
if err = svc.sendPasswordResetToken(u); err != nil {
|
||||
if err = svc.sendPasswordResetToken(ctx, u); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}()
|
||||
|
||||
return svc.recordAction(svc.ctx, aam, AuthActionSendPasswordResetToken, err)
|
||||
return svc.recordAction(ctx, aam, AuthActionSendPasswordResetToken, err)
|
||||
}
|
||||
|
||||
// CanRegister verifies if user can register
|
||||
func (svc auth) CanRegister() error {
|
||||
func (svc auth) CanRegister(ctx context.Context) error {
|
||||
if svc.subscription != nil {
|
||||
c, err := svc.store.CountUsers(ctx, types.UserFilter{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("can not check if user can register: %w", err)
|
||||
}
|
||||
|
||||
// When we have an active subscription, we need to check
|
||||
// if users can register or did this deployment hit
|
||||
// it's user-limit
|
||||
return svc.subscription.CanRegister(svc.users.Total())
|
||||
return svc.subscription.CanRegister(c)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc auth) sendPasswordResetToken(u *types.User) (err error) {
|
||||
func (svc auth) sendPasswordResetToken(ctx context.Context, u *types.User) (err error) {
|
||||
var (
|
||||
notificationLang = "en"
|
||||
)
|
||||
|
||||
token, err := svc.createUserToken(u, credentialsTypeResetPasswordToken)
|
||||
token, err := svc.createUserToken(ctx, u, credentialsTypeResetPasswordToken)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return svc.notifications.PasswordReset(svc.ctx, notificationLang, u.Email, token)
|
||||
return svc.notifications.PasswordReset(ctx, notificationLang, u.Email, token)
|
||||
}
|
||||
|
||||
func (svc auth) loadUserFromToken(token, kind string) (u *types.User, err error) {
|
||||
func (svc auth) loadUserFromToken(ctx context.Context, token, kind string) (u *types.User, err error) {
|
||||
var (
|
||||
aam = &authActionProps{
|
||||
credentials: &types.Credentials{Kind: kind},
|
||||
@@ -931,7 +924,7 @@ func (svc auth) loadUserFromToken(token, kind string) (u *types.User, err error)
|
||||
return nil, AuthErrInvalidToken(aam)
|
||||
}
|
||||
|
||||
c, err := svc.credentials.FindByID(credentialsID)
|
||||
c, err := svc.store.LookupCredentialsByID(ctx, credentialsID)
|
||||
if err == repository.ErrCredentialsNotFound {
|
||||
return nil, AuthErrInvalidToken(aam)
|
||||
}
|
||||
@@ -942,7 +935,7 @@ func (svc auth) loadUserFromToken(token, kind string) (u *types.User, err error)
|
||||
return
|
||||
}
|
||||
|
||||
if err = svc.credentials.DeleteByID(c.ID); err != nil {
|
||||
if err = svc.store.RemoveCredentialsByID(ctx, c.ID); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -950,7 +943,7 @@ func (svc auth) loadUserFromToken(token, kind string) (u *types.User, err error)
|
||||
return nil, AuthErrInvalidToken(aam)
|
||||
}
|
||||
|
||||
u, err = svc.users.FindByID(c.OwnerID)
|
||||
u, err = svc.store.LookupUserByID(ctx, c.OwnerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -984,7 +977,7 @@ func (svc auth) validateToken(token string) (ID uint64, credentials string) {
|
||||
|
||||
// Generates & stores user token
|
||||
// it returns combined value of token + token ID to help with the lookups
|
||||
func (svc auth) createUserToken(u *types.User, kind string) (token string, err error) {
|
||||
func (svc auth) createUserToken(ctx context.Context, u *types.User, kind string) (token string, err error) {
|
||||
var (
|
||||
expiresAt time.Time
|
||||
aam = &authActionProps{
|
||||
@@ -997,18 +990,22 @@ func (svc auth) createUserToken(u *types.User, kind string) (token string, err e
|
||||
switch kind {
|
||||
case credentialsTypeAuthToken:
|
||||
// 15 sec expiration for all tokens that are part of redirection
|
||||
expiresAt = svc.now().Add(time.Second * 15)
|
||||
expiresAt = nowPtr().Add(time.Second * 15)
|
||||
default:
|
||||
// 1h expiration for all tokens send via email
|
||||
expiresAt = svc.now().Add(time.Minute * 60)
|
||||
expiresAt = nowPtr().Add(time.Minute * 60)
|
||||
}
|
||||
|
||||
c, err := svc.credentials.Create(&types.Credentials{
|
||||
c := &types.Credentials{
|
||||
ID: id.Next(),
|
||||
CreatedAt: now(),
|
||||
OwnerID: u.ID,
|
||||
Kind: kind,
|
||||
Credentials: string(rand.Bytes(credentialsTokenLength)),
|
||||
ExpiresAt: &expiresAt,
|
||||
})
|
||||
}
|
||||
|
||||
err := svc.store.CreateCredentials(ctx, c)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -1018,32 +1015,35 @@ func (svc auth) createUserToken(u *types.User, kind string) (token string, err e
|
||||
return nil
|
||||
}()
|
||||
|
||||
return token, svc.recordAction(svc.ctx, aam, AuthActionIssueToken, err)
|
||||
return token, svc.recordAction(ctx, aam, AuthActionIssueToken, err)
|
||||
}
|
||||
|
||||
// Automatically promotes user to administrator if it is the first user in the database
|
||||
func (svc auth) autoPromote(u *types.User) (err error) {
|
||||
if svc.users.Total() > 1 || u.ID == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if svc.roles == nil {
|
||||
// no role repository; auto-promotion disabled
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc auth) autoPromote(ctx context.Context, u *types.User) (err error) {
|
||||
var (
|
||||
c uint
|
||||
roleID = permissions.AdminsRoleID
|
||||
aam = &authActionProps{user: u, role: &types.Role{ID: roleID}}
|
||||
)
|
||||
|
||||
err = svc.roles.MemberAddByID(roleID, u.ID)
|
||||
return svc.recordAction(svc.ctx, aam, AuthActionAutoPromote, err)
|
||||
err = func() error {
|
||||
if c, err = svc.store.CountUsers(ctx, types.UserFilter{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c > 1 || u.ID == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return svc.store.AddRoleMembersByID(ctx, roleID, u.ID)
|
||||
}()
|
||||
|
||||
return svc.recordAction(ctx, aam, AuthActionAutoPromote, err)
|
||||
}
|
||||
|
||||
// LoadRoleMemberships loads membership info
|
||||
func (svc auth) LoadRoleMemberships(u *types.User) error {
|
||||
rr, _, err := svc.roles.Find(types.RoleFilter{MemberID: u.ID})
|
||||
func (svc auth) LoadRoleMemberships(ctx context.Context, u *types.User) error {
|
||||
rr, _, err := svc.store.SearchRoles(ctx, types.RoleFilter{MemberID: u.ID})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+176
-119
@@ -1,159 +1,216 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/cortezaproject/corteza-server/pkg/id"
|
||||
"github.com/cortezaproject/corteza-server/store/sqlite"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/markbates/goth"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/markbates/goth"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/pkg/eventbus"
|
||||
"github.com/cortezaproject/corteza-server/system/repository"
|
||||
repomock "github.com/cortezaproject/corteza-server/system/repository/mocks"
|
||||
"github.com/cortezaproject/corteza-server/system/types"
|
||||
)
|
||||
|
||||
// @todo this mockDB will be probably be used by other tests, move it to some common place
|
||||
type mockDB struct{}
|
||||
|
||||
func (mockDB) Transaction(callback func() error) error { return callback() }
|
||||
|
||||
// Mock auth service with nil for current time, dummy provider validator and mock db
|
||||
func makeMockAuthService(u repository.UserRepository, c repository.CredentialsRepository) *auth {
|
||||
return &auth{
|
||||
db: &mockDB{},
|
||||
users: u,
|
||||
credentials: c,
|
||||
func makeMockAuthService() *auth {
|
||||
var (
|
||||
ctx = context.Background()
|
||||
|
||||
providerValidator: func(s string) error {
|
||||
// All providers are valid.
|
||||
return nil
|
||||
},
|
||||
mem, err = sqlite.NewInMemory(ctx)
|
||||
|
||||
settings: &types.Settings{},
|
||||
svc = &auth{
|
||||
providerValidator: func(s string) error {
|
||||
// All providers are valid.
|
||||
return nil
|
||||
},
|
||||
|
||||
eventbus: eventbus.New(),
|
||||
settings: &types.AppSettings{},
|
||||
eventbus: eventbus.New(),
|
||||
}
|
||||
)
|
||||
|
||||
now: func() *time.Time {
|
||||
return nil
|
||||
},
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err = mem.Upgrade(ctx, zap.NewNop()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
svc.store = mem
|
||||
|
||||
return svc
|
||||
}
|
||||
|
||||
func TestAuth_External_Existing(t *testing.T) {
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
func TestAuth_External(t *testing.T) {
|
||||
var (
|
||||
req = require.New(t)
|
||||
ctx = context.Background()
|
||||
|
||||
// Create some virtual user and credentials
|
||||
var u = &types.User{ID: 300000, Email: "foo@example.tld"}
|
||||
var c = &types.Credentials{ID: 200000, OwnerID: u.ID}
|
||||
// Create some virtual user and credentials
|
||||
validUser = &types.User{Email: "valid@test.cortezaproject.org", ID: id.Next(), CreatedAt: now()}
|
||||
suspendedUser = &types.User{Email: "suspended@test.cortezaproject.org", ID: id.Next(), CreatedAt: now(), SuspendedAt: nowPtr()}
|
||||
|
||||
// Profile to be used. make sure email matches
|
||||
var p = goth.User{UserID: "some-profile-id", Provider: "google", Email: u.Email}
|
||||
freshProfileID = func() string {
|
||||
return fmt.Sprintf("fresh-profile-id-%d", id.Next())
|
||||
}
|
||||
|
||||
crdRpoMock := repomock.NewMockCredentialsRepository(mockCtrl)
|
||||
crdRpoMock.EXPECT().
|
||||
FindByCredentials("google", p.UserID).
|
||||
Times(1).
|
||||
Return(types.CredentialsSet{c}, nil)
|
||||
fooCredentials = &types.Credentials{
|
||||
ID: id.Next(),
|
||||
OwnerID: validUser.ID,
|
||||
Label: "credentials for foo provider",
|
||||
Kind: "foo",
|
||||
Credentials: freshProfileID(),
|
||||
CreatedAt: time.Time{},
|
||||
}
|
||||
|
||||
crdRpoMock.EXPECT().
|
||||
Update(gomock.Any()).
|
||||
Times(1).
|
||||
Return(c, nil)
|
||||
barCredentials = &types.Credentials{
|
||||
ID: id.Next(),
|
||||
OwnerID: validUser.ID,
|
||||
Label: "credentials for bar provider",
|
||||
Kind: "bar",
|
||||
Credentials: freshProfileID(),
|
||||
CreatedAt: time.Time{},
|
||||
}
|
||||
|
||||
usrRpoMock := repomock.NewMockUserRepository(mockCtrl)
|
||||
usrRpoMock.EXPECT().FindByID(u.ID).Times(1).Return(u, nil)
|
||||
cases = []struct {
|
||||
name string
|
||||
profile goth.User
|
||||
user *types.User
|
||||
err error
|
||||
}{
|
||||
{
|
||||
"matching by user email",
|
||||
goth.User{UserID: freshProfileID(), Provider: "-", Email: validUser.Email},
|
||||
validUser,
|
||||
nil},
|
||||
{
|
||||
"unknown profile",
|
||||
goth.User{UserID: freshProfileID(), Provider: "-", Email: "fresh-from-foo@test.cortezaproject.org"},
|
||||
&types.User{Email: "fresh-from-foo@test.cortezaproject.org"},
|
||||
nil},
|
||||
{
|
||||
"profile match by provider ID",
|
||||
goth.User{UserID: fooCredentials.Credentials, Provider: fooCredentials.Kind, Email: "valid+2nd+email@test.cortezaproject.org"},
|
||||
validUser,
|
||||
nil},
|
||||
}
|
||||
)
|
||||
|
||||
svc := makeMockAuthService(usrRpoMock, crdRpoMock)
|
||||
_ = spew.Dump
|
||||
|
||||
svc := makeMockAuthService()
|
||||
svc.settings.Auth.External.Enabled = true
|
||||
req.NoError(svc.store.TruncateUsers(ctx))
|
||||
req.NoError(svc.store.TruncateCredentials(ctx))
|
||||
req.NoError(svc.store.CreateUser(ctx, validUser, suspendedUser))
|
||||
req.NoError(svc.store.CreateCredentials(ctx, fooCredentials, barCredentials))
|
||||
|
||||
{
|
||||
auser, err := svc.External(p)
|
||||
require.NoError(t, err)
|
||||
require.True(t, auser.ID == u.ID, "Did not receive expected user")
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
req = require.New(t)
|
||||
|
||||
var (
|
||||
ru, rerr = svc.External(ctx, c.profile)
|
||||
)
|
||||
|
||||
if c.err != nil {
|
||||
req.EqualError(unwrapGeneric(rerr), c.err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
req.NoError(unwrapGeneric(rerr))
|
||||
req.NotNil(ru)
|
||||
|
||||
if c.user == nil {
|
||||
panic("invalid test case, user should not be nil")
|
||||
}
|
||||
|
||||
req.Equal(c.user.Email, ru.Email)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuth_External_NonExisting(t *testing.T) {
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
|
||||
var u = &types.User{ID: 300000, Email: "foo@example.tld"}
|
||||
var c = &types.Credentials{ID: 200000, OwnerID: u.ID}
|
||||
var p = goth.User{UserID: "some-profile-id", Provider: "google", Email: u.Email}
|
||||
|
||||
crdRpoMock := repomock.NewMockCredentialsRepository(mockCtrl)
|
||||
crdRpoMock.EXPECT().
|
||||
FindByCredentials("google", p.UserID).
|
||||
Times(1).
|
||||
Return(types.CredentialsSet{}, nil)
|
||||
|
||||
crdRpoMock.EXPECT().
|
||||
Create(&types.Credentials{Kind: "google", OwnerID: u.ID, Credentials: p.UserID}).
|
||||
Times(1).
|
||||
Return(c, nil)
|
||||
|
||||
usrRpoMock := repomock.NewMockUserRepository(mockCtrl)
|
||||
usrRpoMock.EXPECT().
|
||||
FindByHandle("foo").
|
||||
Times(1).
|
||||
Return(nil, repository.ErrUserNotFound)
|
||||
|
||||
usrRpoMock.EXPECT().
|
||||
FindByEmail(u.Email).
|
||||
Times(1).
|
||||
Return(nil, repository.ErrUserNotFound)
|
||||
|
||||
usrRpoMock.EXPECT().
|
||||
Create(&types.User{Email: "foo@example.tld", Handle: "foo"}).
|
||||
Times(1).
|
||||
Return(u, nil)
|
||||
|
||||
usrRpoMock.EXPECT().
|
||||
Total().
|
||||
Times(1).
|
||||
Return(uint(0))
|
||||
|
||||
svc := makeMockAuthService(usrRpoMock, crdRpoMock)
|
||||
svc.settings.Auth.External.Enabled = true
|
||||
|
||||
{
|
||||
auser, err := svc.External(p)
|
||||
require.NoError(t, err)
|
||||
require.True(t, auser.ID == u.ID, "Did not receive expected user")
|
||||
}
|
||||
func TestAuth_InternalSignU(t *testing.T) {
|
||||
t.Skip("pending implementation")
|
||||
}
|
||||
|
||||
func Test_auth_validateInternalLogin(t *testing.T) {
|
||||
type args struct {
|
||||
email string
|
||||
password string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "no email", args: args{"", ""}, wantErr: true},
|
||||
{name: "bad email", args: args{"test", ""}, wantErr: true},
|
||||
{name: "no pass", args: args{"test@domain.tld", ""}, wantErr: true},
|
||||
//{name: "all good", args: args{"test@domain.tld", "password"}, wantErr: false},
|
||||
func TestAuth_InternalLogin(t *testing.T) {
|
||||
var (
|
||||
req = require.New(t)
|
||||
ctx = context.Background()
|
||||
|
||||
// until we get proper mocking, DI for unit testing in place
|
||||
// this will have to do
|
||||
}
|
||||
validPass = "this is a valid password !! 42"
|
||||
validUser = &types.User{Email: "valid@test.cortezaproject.org", ID: id.Next(), CreatedAt: now(), EmailConfirmed: true}
|
||||
suspendedUser = &types.User{Email: "suspended@test.cortezaproject.org", ID: id.Next(), CreatedAt: now(), SuspendedAt: nowPtr()}
|
||||
|
||||
svc := makeMockAuthService(nil, nil)
|
||||
tests = []struct {
|
||||
name string
|
||||
email string
|
||||
password string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
"with no email",
|
||||
"",
|
||||
"",
|
||||
fmt.Errorf("invalid email")},
|
||||
{
|
||||
"with bad email",
|
||||
"test",
|
||||
"",
|
||||
fmt.Errorf("invalid email")},
|
||||
{
|
||||
"with empty password",
|
||||
"test@domain.tld",
|
||||
"",
|
||||
fmt.Errorf("invalid username and password combination")},
|
||||
{
|
||||
"with valid credentials",
|
||||
validUser.Email,
|
||||
validPass,
|
||||
nil},
|
||||
{
|
||||
"with invalid password",
|
||||
validUser.Email,
|
||||
"invalid password",
|
||||
fmt.Errorf("invalid username and password combination")},
|
||||
{
|
||||
"with suspended user",
|
||||
suspendedUser.Email,
|
||||
validPass,
|
||||
fmt.Errorf("invalid username and password combination")},
|
||||
}
|
||||
)
|
||||
|
||||
svc := makeMockAuthService()
|
||||
svc.settings.Auth.Internal.Enabled = true
|
||||
req.NoError(svc.store.TruncateUsers(ctx))
|
||||
req.NoError(svc.store.TruncateCredentials(ctx))
|
||||
req.NoError(svc.store.CreateUser(ctx, validUser, suspendedUser))
|
||||
req.NoError(svc.SetPasswordCredentials(ctx, validUser.ID, validPass))
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if _, err := svc.InternalLogin(tt.args.email, tt.args.password); (err != nil) != tt.wantErr {
|
||||
t.Errorf("auth.validateInternalLogin() error = %v, wantErr %v", err, tt.wantErr)
|
||||
req = require.New(t)
|
||||
|
||||
var (
|
||||
usr, err = svc.InternalLogin(ctx, tt.email, tt.password)
|
||||
)
|
||||
|
||||
if tt.err == nil {
|
||||
req.NoError(err)
|
||||
req.NotNil(usr)
|
||||
} else {
|
||||
req.EqualError(err, tt.err.Error())
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -206,7 +263,7 @@ func Test_auth_checkPassword(t *testing.T) {
|
||||
}
|
||||
|
||||
svc := auth{
|
||||
settings: &types.Settings{},
|
||||
settings: &types.AppSettings{},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/cortezaproject/corteza-server/pkg/healthcheck"
|
||||
"github.com/cortezaproject/corteza-server/pkg/id"
|
||||
@@ -85,7 +86,7 @@ var (
|
||||
|
||||
DefaultSink *sink
|
||||
|
||||
DefaultAuth AuthService
|
||||
DefaultAuth *auth
|
||||
DefaultUser UserService
|
||||
DefaultRole RoleService
|
||||
DefaultOrganisation OrganisationService
|
||||
@@ -188,7 +189,7 @@ func Initialize(ctx context.Context, log *zap.Logger, s interface{}, c Config) (
|
||||
hcd.Add(store.Healthcheck(DefaultStore), "Store/System")
|
||||
|
||||
DefaultAuthNotification = AuthNotification(CurrentSettings)
|
||||
DefaultAuth = Auth(ctx)
|
||||
DefaultAuth = Auth()
|
||||
DefaultUser = User(ctx)
|
||||
DefaultRole = Role(ctx)
|
||||
DefaultOrganisation = Organisation(ctx)
|
||||
@@ -215,3 +216,21 @@ func Watchers(ctx context.Context) {
|
||||
// Reloading permissions on change
|
||||
DefaultPermissions.Watch(ctx)
|
||||
}
|
||||
|
||||
// isGeneric returns true if given error is generic
|
||||
func isGeneric(err error) bool {
|
||||
g, ok := err.(interface{ IsGeneric() bool })
|
||||
return ok && g != nil && g.IsGeneric()
|
||||
}
|
||||
|
||||
// unwrapGeneric unwraps error if error is generic (and wrapped)
|
||||
func unwrapGeneric(err error) error {
|
||||
for {
|
||||
if isGeneric(err) {
|
||||
err = errors.Unwrap(err)
|
||||
continue
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
+10
-9
@@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/cortezaproject/corteza-server/store"
|
||||
"io"
|
||||
"net/mail"
|
||||
"regexp"
|
||||
@@ -47,8 +48,8 @@ type (
|
||||
}
|
||||
|
||||
userAuth interface {
|
||||
checkPasswordStrength(string) bool
|
||||
changePassword(uint64, string) error
|
||||
CheckPasswordStrength(string) bool
|
||||
SetPasswordCredentials(context.Context, uint64, string) error
|
||||
}
|
||||
|
||||
userSubscriptionChecker interface {
|
||||
@@ -328,7 +329,7 @@ func (svc user) Create(new *types.User) (u *types.User, err error) {
|
||||
}
|
||||
|
||||
if new.Handle == "" {
|
||||
createHandle(svc.user, new)
|
||||
createHandle(svc.ctx, DefaultNgStore, new)
|
||||
}
|
||||
|
||||
if err = svc.UniqueCheck(new); err != nil {
|
||||
@@ -623,11 +624,11 @@ func (svc user) SetPassword(userID uint64, newPassword string) (err error) {
|
||||
return UserErrNotAllowedToUpdate()
|
||||
}
|
||||
|
||||
if !svc.auth.checkPasswordStrength(newPassword) {
|
||||
if !svc.auth.CheckPasswordStrength(newPassword) {
|
||||
return UserErrPasswordNotSecure()
|
||||
}
|
||||
|
||||
if err := svc.auth.changePassword(userID, newPassword); err != nil {
|
||||
if err := svc.auth.SetPasswordCredentials(svc.ctx, userID, newPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -699,13 +700,13 @@ rangeLoop:
|
||||
return uu.Walk(s)
|
||||
}
|
||||
|
||||
func createHandle(r repository.UserRepository, u *types.User) {
|
||||
func createHandle(ctx context.Context, s usersStore, u *types.User) {
|
||||
if u.Handle == "" {
|
||||
u.Handle, _ = handle.Cast(
|
||||
// Must not exist before
|
||||
func(s string) bool {
|
||||
e, err := r.FindByHandle(s)
|
||||
return err == repository.ErrUserNotFound && (e == nil || e.ID == u.ID)
|
||||
func(lookup string) bool {
|
||||
e, err := s.LookupUserByHandle(ctx, lookup)
|
||||
return err == store.ErrNotFound && (e == nil || e.ID == u.ID)
|
||||
},
|
||||
// use name or username
|
||||
u.Name,
|
||||
|
||||
Reference in New Issue
Block a user