3
0

Ported system user service to new store

This commit is contained in:
Denis Arh
2020-08-16 06:23:31 +02:00
parent 0af3f68d16
commit 783cddd523
12 changed files with 577 additions and 416 deletions
+8
View File
@@ -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
View File
@@ -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,
+10
View File
@@ -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)
}
}
+37 -1
View File
@@ -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)
})
}
+4 -4
View File
@@ -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
View File
@@ -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
}
+19 -17
View File
@@ -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 -23
View File
@@ -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
View File
@@ -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
View File
@@ -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 {
+21 -2
View File
@@ -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
View File
@@ -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,