Skip to content

Commit 4256af3

Browse files
committed
wip
1 parent 9bff9bf commit 4256af3

File tree

6 files changed

+227
-37
lines changed

6 files changed

+227
-37
lines changed

api/services/auth.go

Lines changed: 139 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,18 @@ package services
33
import (
44
"context"
55
"crypto/rsa"
6+
"slices"
7+
"strings"
68
"time"
79

10+
"github.com/shellhub-io/shellhub/pkg/api/authorizer"
11+
"github.com/shellhub-io/shellhub/pkg/api/jwttoken"
812
"github.com/shellhub-io/shellhub/pkg/api/requests"
13+
"github.com/shellhub-io/shellhub/pkg/clock"
14+
"github.com/shellhub-io/shellhub/pkg/hash"
915
"github.com/shellhub-io/shellhub/pkg/models"
16+
"github.com/shellhub-io/shellhub/pkg/uuid"
17+
log "github.com/sirupsen/logrus"
1018
)
1119

1220
type AuthService interface {
@@ -54,7 +62,137 @@ func (s *service) AuthDevice(ctx context.Context, req requests.DeviceAuth, remot
5462
}
5563

5664
func (s *service) AuthLocalUser(ctx context.Context, req *requests.AuthLocalUser, sourceIP string) (*models.UserAuthResponse, int64, string, error) {
57-
return nil, 0, "", nil
65+
// if s, err := s.store.SystemGet(ctx); err != nil || !s.Authentication.Local.Enabled {
66+
// return nil, 0, "", NewErrAuthMethodNotAllowed(models.UserAuthMethodLocal.String())
67+
// }
68+
69+
var err error
70+
var user *models.User
71+
72+
if req.Identifier.IsEmail() {
73+
user, err = s.store.UserGetByEmail(ctx, strings.ToLower(string(req.Identifier)))
74+
} else {
75+
user, err = s.store.UserGetByUsername(ctx, strings.ToLower(string(req.Identifier)))
76+
}
77+
78+
if err != nil || !slices.Contains(user.Preferences.AuthMethods, models.UserAuthMethodLocal) {
79+
return nil, 0, "", NewErrAuthUnathorized(nil)
80+
}
81+
82+
switch user.Status {
83+
case models.UserStatusInvited:
84+
return nil, 0, "", NewErrAuthUnathorized(nil)
85+
case models.UserStatusNotConfirmed:
86+
return nil, 0, "", NewErrUserNotConfirmed(nil)
87+
default:
88+
break
89+
}
90+
91+
// Checks whether the user is currently blocked from new login attempts
92+
if lockout, attempt, _ := s.cache.HasAccountLockout(ctx, sourceIP, user.ID); lockout > 0 {
93+
log.WithFields(log.Fields{
94+
"lockout": lockout,
95+
"attempt": attempt,
96+
"source_ip": sourceIP,
97+
"user_id": user.ID,
98+
}).
99+
Warn("attempt to login blocked")
100+
101+
return nil, lockout, "", NewErrAuthUnathorized(nil)
102+
}
103+
104+
if !hash.CompareWith(req.Password, user.Password) {
105+
lockout, _, err := s.cache.StoreLoginAttempt(ctx, sourceIP, user.ID)
106+
if err != nil {
107+
log.WithError(err).
108+
WithField("source_ip", sourceIP).
109+
WithField("user_id", user.ID).
110+
Warn("unable to store login attempt")
111+
}
112+
113+
return nil, lockout, "", NewErrAuthUnathorized(nil)
114+
}
115+
116+
// Reset the attempt and timeout values when succeeds
117+
if err := s.cache.ResetLoginAttempts(ctx, sourceIP, user.ID); err != nil {
118+
log.WithError(err).
119+
WithField("source_ip", sourceIP).
120+
WithField("user_id", user.ID).
121+
Warn("unable to reset authentication attempts")
122+
}
123+
124+
// Users with MFA enabled must authenticate to the cloud instead of community.
125+
if user.MFA.Enabled {
126+
mfaToken := uuid.Generate()
127+
if err := s.cache.Set(ctx, "mfa-token={"+mfaToken+"}", user.ID, 30*time.Minute); err != nil {
128+
log.WithError(err).
129+
WithField("source_ip", sourceIP).
130+
WithField("user_id", user.ID).
131+
Warn("unable to store mfa-token")
132+
}
133+
134+
return nil, 0, mfaToken, nil
135+
}
136+
137+
tenantID := ""
138+
role := ""
139+
// Populate the tenant and role when the user is associated with a namespace. If the member status is pending, we
140+
// ignore the namespace.
141+
if ns, _ := s.store.NamespaceGetPreferred(ctx, user.ID); ns != nil && ns.TenantID != "" {
142+
if m, _ := ns.FindMember(user.ID); m.Status != models.MemberStatusPending {
143+
tenantID = ns.TenantID
144+
role = m.Role.String()
145+
}
146+
}
147+
148+
claims := authorizer.UserClaims{
149+
ID: user.ID,
150+
Origin: user.Origin.String(),
151+
TenantID: tenantID,
152+
Username: user.Username,
153+
MFA: user.MFA.Enabled,
154+
}
155+
156+
token, err := jwttoken.EncodeUserClaims(claims, s.privKey)
157+
if err != nil {
158+
return nil, 0, "", NewErrTokenSigned(err)
159+
}
160+
161+
// Updates last_login and the hash algorithm to bcrypt if still using SHA256
162+
changes := &models.UserChanges{LastLogin: clock.Now(), PreferredNamespace: &tenantID}
163+
if !strings.HasPrefix(user.Password, "$") {
164+
if neo, _ := models.HashUserPassword(req.Password); neo.Hash != "" {
165+
changes.Password = neo.Hash
166+
}
167+
}
168+
169+
// TODO: evaluate make this update in a go routine.
170+
if err := s.store.UserUpdate(ctx, user.ID, changes); err != nil {
171+
return nil, 0, "", NewErrUserUpdate(user, err)
172+
}
173+
174+
if err := s.AuthCacheToken(ctx, tenantID, user.ID, token); err != nil {
175+
log.WithError(err).
176+
WithFields(log.Fields{"id": user.ID}).
177+
Warn("unable to cache the authentication token")
178+
}
179+
180+
res := &models.UserAuthResponse{
181+
ID: user.ID,
182+
Origin: user.Origin.String(),
183+
AuthMethods: user.Preferences.AuthMethods,
184+
User: user.Username,
185+
Name: user.Name,
186+
Email: user.Email,
187+
RecoveryEmail: user.RecoveryEmail,
188+
MFA: user.MFA.Enabled,
189+
Tenant: tenantID,
190+
Role: role,
191+
Token: token,
192+
MaxNamespaces: user.MaxNamespaces,
193+
}
194+
195+
return res, 0, "", nil
58196
}
59197

60198
func (s *service) CreateUserToken(ctx context.Context, req *requests.CreateUserToken) (*models.UserAuthResponse, error) {

api/services/system.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ type SystemService interface {
1515
}
1616

1717
func (s *service) GetSystemInfo(ctx context.Context, req *requests.GetSystemInfo) (*responses.SystemInfo, error) {
18-
return nil, nil
18+
return &responses.SystemInfo{Setup: true, Authentication: &responses.SystemAuthenticationInfo{Local: true}}, nil
1919
}
2020

2121
func (s *service) SystemDownloadInstallScript(_ context.Context) (string, error) {
Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,42 @@
11
BEGIN;
22

3-
CREATE TYPE user_origin AS ENUM ('local', 'saml');
4-
CREATE TYPE user_status AS ENUM ('invited', 'pending', 'confirmed');
3+
DO $$
4+
BEGIN
5+
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'user_origin') THEN
6+
CREATE TYPE user_origin AS ENUM ('local', 'saml');
7+
END IF;
8+
9+
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'user_status') THEN
10+
CREATE TYPE user_status AS ENUM ('invited', 'pending', 'confirmed');
11+
END IF;
12+
13+
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'user_auth_method') THEN
14+
CREATE TYPE user_auth_method AS ENUM ('local', 'saml');
15+
END IF;
16+
END
17+
$$;
18+
519

620
CREATE TABLE IF NOT EXISTS users(
7-
id CHAR(40) PRIMARY KEY,
21+
id UUID PRIMARY KEY,
822

9-
created_at TIMESTAMPTZ,
10-
updated_at TIMESTAMPTZ,
23+
created_at TIMESTAMPTZ NOT NULL,
24+
updated_at TIMESTAMPTZ NOT NULL,
25+
last_login TIMESTAMPTZ,
1126

1227
origin user_origin NOT NULL,
28+
external_id VARCHAR,
1329
status user_status NOT NULL,
14-
name VARCHAR (50) NOT NULL,
15-
email VARCHAR (300) UNIQUE NOT NULL,
16-
password VARCHAR (72) NOT NULL,
17-
external_id VARCHAR
30+
name VARCHAR(64) NOT NULL,
31+
username VARCHAR(32) UNIQUE NOT NULL,
32+
email VARCHAR(320) UNIQUE NOT NULL,
33+
security_email VARCHAR(320),
34+
password_digest CHAR(72) NOT NULL,
35+
auth_methods user_auth_method[] NOT NULL,
36+
37+
namespace_ownership_limit INTEGER NOT NULL,
38+
email_marketing BOOLEAN NOT NULL,
39+
preferred_namespace_id UUID
1840
);
1941

2042
COMMIT;

api/store/pg/user.go

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
)
1212

1313
func (pg *pg) UserCreate(ctx context.Context, user *models.User) (string, error) {
14-
user.ID = "USR|" + uuid.Generate()
14+
user.ID = uuid.Generate()
1515
user.CreatedAt = clock.Now()
1616
user.UpdatedAt = clock.Now()
1717

@@ -48,22 +48,30 @@ func (pg *pg) UserList(ctx context.Context, paginator query.Paginator, filters q
4848
}
4949

5050
func (pg *pg) UserGetByID(ctx context.Context, id string, ns bool) (*models.User, int, error) {
51-
// TODO: unify get methods
52-
return nil, 0, nil
51+
u := new(entity.User)
52+
if err := pg.driver.NewSelect().Model(u).Where("id = ?", id).Scan(ctx); err != nil {
53+
return nil, 0, fromSqlError(err)
54+
}
55+
56+
return &u.User, 0, nil
5357
}
5458

5559
func (pg *pg) UserGetByUsername(ctx context.Context, username string) (*models.User, error) {
56-
// TODO: unify get methods
57-
return nil, nil
60+
u := new(entity.User)
61+
if err := pg.driver.NewSelect().Model(u).Where("username = ?", username).Scan(ctx); err != nil {
62+
return nil, fromSqlError(err)
63+
}
64+
65+
return &u.User, nil
5866
}
5967

6068
func (pg *pg) UserGetByEmail(ctx context.Context, email string) (*models.User, error) {
61-
u := new(models.User)
69+
u := new(entity.User)
6270
if err := pg.driver.NewSelect().Model(u).Where("email = ?", email).Scan(ctx); err != nil {
6371
return nil, fromSqlError(err)
6472
}
6573

66-
return u, nil
74+
return &u.User, nil
6775
}
6876

6977
func (pg *pg) UserGetInfo(ctx context.Context, id string) (userInfo *models.UserInfo, err error) {

cli/services/users.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,20 @@ func (s *service) UserCreate(ctx context.Context, input *inputs.UserCreate) (*mo
3636
}
3737

3838
user := &models.User{
39-
Status: models.UserStatusConfirmed,
4039
Origin: models.UserOriginLocal,
4140
ExternalID: "",
41+
Status: models.UserStatusConfirmed,
4242
Name: cases.Title(language.AmericanEnglish).String(strings.ToLower(input.Username)),
4343
Email: strings.ToLower(input.Email),
44+
Username: strings.ToLower(input.Username),
4445
Password: password.Hash,
46+
Preferences: models.UserPreferences{
47+
PreferredNamespace: "",
48+
AuthMethods: []models.UserAuthMethod{models.UserAuthMethodLocal},
49+
RecoveryEmail: "",
50+
MaxNamespaces: -1,
51+
EmailMarketing: false,
52+
},
4553
}
4654

4755
if _, err := s.store.UserCreate(ctx, user); err != nil {

pkg/models/user.go

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -59,25 +59,39 @@ func (a UserAuthMethod) String() string {
5959
type User struct {
6060
ID string `json:"id,omitempty" bson:"_id,omitempty" bun:"id,pk,type:char"`
6161

62-
CreatedAt time.Time `json:"created_at" bson:"created_at" bun:"created_at"`
63-
UpdatedAt time.Time `json:"updated_at" bson:"updated_at" bun:"updated_at"`
64-
Status UserStatus `json:"status" bson:"status" bun:"status"`
62+
// CreatedAt represents timestamp when the user was created
63+
CreatedAt time.Time `json:"created_at" bson:"created_at" bun:"created_at"`
64+
// UpdatedAt represents timestamp when the user was last updated
65+
UpdatedAt time.Time `json:"updated_at" bson:"updated_at" bun:"updated_at"`
66+
// LastLogin represents timestamp of the user's most recent login
67+
LastLogin time.Time `json:"last_login" bson:"last_login" bun:"last_login,nullzero"`
68+
6569
// Origin specifies the the user's signup method.
6670
Origin UserOrigin `json:"-" bson:"origin" bun:"origin"`
6771
// ExternalID represents the user's identifier in an external system. It is always empty when [User.Origin]
6872
// is [UserOriginLocal].
6973
ExternalID string `json:"-" bson:"external_id" bun:"external_id,nullzero"`
70-
Name string `json:"name" validate:"required,name" bun:"name"`
71-
Email string `json:"email" bson:"email" validate:"required,email" bun:"email"`
72-
Password string `json:"-" bson:",inline" bun:"password"`
7374

74-
MFA UserMFA `json:"mfa" bson:"mfa" bun:"-"`
75-
Preferences UserPreferences `json:"preferences" bson:"preferences" bun:"-"`
76-
RecoveryEmail string `json:"recovery_email" bson:"recovery_email" validate:"omitempty,email" bun:"-"`
77-
MaxNamespaces int `json:"max_namespaces" bson:"max_namespaces" bun:"-"`
78-
EmailMarketing bool `json:"email_marketing" bson:"email_marketing" bun:"-"`
79-
Username string `json:"username" bson:"username" validate:"required,username" bun:"-"`
80-
LastLogin time.Time `json:"last_login" bson:"last_login" bun:"-"`
75+
Status UserStatus `json:"status" bson:"status" bun:"status"`
76+
77+
Name string `json:"name" validate:"required,name" bun:"name"`
78+
Username string `json:"username" bson:"username" validate:"required,username" bun:"username"`
79+
Email string `json:"email" bson:"email" validate:"required,email" bun:"email"`
80+
Password string `json:"-" bson:",inline" bun:"password_digest"`
81+
82+
Preferences UserPreferences `json:"preferences" bson:"preferences" bun:"embed:"`
83+
84+
///
85+
///
86+
///
87+
///
88+
///
89+
///
90+
91+
MFA UserMFA `json:"mfa" bson:"mfa" bun:"-"`
92+
RecoveryEmail string `json:"recovery_email" bson:"recovery_email" validate:"omitempty,email" bun:"-"`
93+
MaxNamespaces int `json:"max_namespaces" bson:"max_namespaces" bun:"-"`
94+
EmailMarketing bool `json:"email_marketing" bson:"email_marketing" bun:"-"`
8195
}
8296

8397
type UserData struct {
@@ -102,11 +116,11 @@ type UserMFA struct {
102116
}
103117

104118
type UserPreferences struct {
105-
// PreferredNamespace represents the namespace the user most recently authenticated with.
106-
PreferredNamespace string `json:"-" bson:"preferred_namespace"`
107-
108-
// AuthMethods indicates the authentication methods that the user can use to authenticate.
109-
AuthMethods []UserAuthMethod `json:"auth_methods" bson:"auth_methods"`
119+
PreferredNamespace string `json:"-" bson:"preferred_namespace" bun:"preferred_namespace_id,nullzero"`
120+
AuthMethods []UserAuthMethod `json:"auth_methods" bson:"auth_methods" bun:"auth_methods,array"`
121+
RecoveryEmail string `json:"recovery_email" bson:"recovery_email" validate:"omitempty,email" bun:"security_email,nullzero"`
122+
MaxNamespaces int `json:"max_namespaces" bson:"max_namespaces" bun:"namespace_ownership_limit"`
123+
EmailMarketing bool `json:"email_marketing" bson:"email_marketing" bun:"email_marketing"`
110124
}
111125

112126
type UserPassword struct {

0 commit comments

Comments
 (0)