Skip to content

Commit 8d68a22

Browse files
committed
wip(api): more things
1 parent 28363b0 commit 8d68a22

File tree

25 files changed

+259
-1101
lines changed

25 files changed

+259
-1101
lines changed

api/Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ COPY ./api/entrypoint-dev.sh /entrypoint.sh
5454
WORKDIR $GOPATH/src/github.com/shellhub-io/shellhub/api
5555

5656
RUN mkdir -p /templates
57+
RUN mkdir -p /migrations
5758

5859
COPY ./install.sh /templates/install.sh
5960

api/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ require (
1212
github.com/jackc/pgx/v5 v5.5.4
1313
github.com/labstack/echo/v4 v4.13.3
1414
github.com/labstack/gommon v0.4.2
15+
github.com/oiime/logrusbun v0.1.2-0.20241011112815-4df3a0fb0e11
1516
github.com/pkg/errors v0.9.1
1617
github.com/shellhub-io/mongotest v0.0.0-20230928124937-e33b07010742
1718
github.com/shellhub-io/shellhub v0.13.4

api/go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,8 @@ github.com/nwaples/rardecode/v2 v2.0.0-beta.2/go.mod h1:yntwv/HfMc/Hbvtq9I19D1n5
270270
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
271271
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
272272
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
273+
github.com/oiime/logrusbun v0.1.2-0.20241011112815-4df3a0fb0e11 h1:rAqW9sGcM0VsfBwgeBzHk0yebrRwfeSJFy9Egqi0fmM=
274+
github.com/oiime/logrusbun v0.1.2-0.20241011112815-4df3a0fb0e11/go.mod h1:HH9akx9teKgQPX41TYpLLRNxaL8q9R+ltzABnwUHfBM=
273275
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
274276
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
275277
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
@@ -326,6 +328,7 @@ github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFt
326328
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
327329
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
328330
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
331+
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
329332
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
330333
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
331334
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
@@ -378,6 +381,7 @@ github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GH
378381
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
379382
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
380383
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
384+
github.com/uptrace/bun v0.3.9/go.mod h1:aL6D9vPw8DXaTQTwGrEPtUderBYXx7ShUmPfnxnqscw=
381385
github.com/uptrace/bun v1.2.11 h1:l9dTymsdZZAoSZ1+Qo3utms0RffgkDbIv+1UGk8N1wQ=
382386
github.com/uptrace/bun v1.2.11/go.mod h1:ww5G8h59UrOnCHmZ8O1I/4Djc7M/Z3E+EWFS2KLB6dQ=
383387
github.com/uptrace/bun/dialect/pgdialect v1.2.11 h1:n0VKWm1fL1dwJK5TRxYYLaRKRe14BOg2+AQgpvqzG/M=

api/server.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ func (s *Server) Setup(ctx context.Context) error {
8484
log.Debug("Redis cache initialized successfully")
8585

8686
uri := pg.URI(s.env.PostgresHost, s.env.PostgresPort, s.env.PostgresUser, s.env.PostgresPassword, s.env.PostgresDB)
87-
store, err := pg.New(ctx, uri, options.Migrate())
87+
store, err := pg.New(ctx, uri, options.Migrate(), options.Log("DEBUG", true))
8888
if err != nil {
8989
return err
9090
}

api/services/auth.go

Lines changed: 5 additions & 288 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,11 @@ package services
22

33
import (
44
"context"
5-
"crypto"
6-
"crypto/rand"
75
"crypto/rsa"
8-
"crypto/sha256"
9-
"crypto/x509"
10-
"encoding/base64"
11-
"encoding/hex"
12-
"encoding/pem"
13-
"slices"
14-
"strings"
156
"time"
167

17-
"github.com/shellhub-io/shellhub/pkg/api/authorizer"
18-
"github.com/shellhub-io/shellhub/pkg/api/jwttoken"
198
"github.com/shellhub-io/shellhub/pkg/api/requests"
20-
"github.com/shellhub-io/shellhub/pkg/clock"
219
"github.com/shellhub-io/shellhub/pkg/models"
22-
"github.com/shellhub-io/shellhub/pkg/uuid"
23-
log "github.com/sirupsen/logrus"
2410
)
2511

2612
type AuthService interface {
@@ -68,292 +54,23 @@ func (s *service) AuthDevice(ctx context.Context, req requests.DeviceAuth, remot
6854
}
6955

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

20960
func (s *service) CreateUserToken(ctx context.Context, req *requests.CreateUserToken) (*models.UserAuthResponse, error) {
210-
user, _, err := s.store.UserGetByID(ctx, req.UserID, false)
211-
if err != nil {
212-
return nil, NewErrUserNotFound(req.UserID, err)
213-
}
214-
215-
tenantID := ""
216-
role := ""
217-
218-
switch req.TenantID {
219-
case "":
220-
// A user may not have a preferred namespace. In such cases, we create a token without it.
221-
namespace, err := s.store.NamespaceGetPreferred(ctx, user.ID)
222-
if err != nil {
223-
break
224-
}
225-
226-
member, ok := namespace.FindMember(user.ID)
227-
if !ok {
228-
return nil, NewErrNamespaceMemberNotFound(user.ID, nil)
229-
}
230-
231-
if member.Status != models.MemberStatusPending {
232-
tenantID = namespace.TenantID
233-
role = member.Role.String()
234-
}
235-
default:
236-
namespace, err := s.store.NamespaceGet(ctx, req.TenantID)
237-
if err != nil {
238-
return nil, NewErrNamespaceNotFound(req.TenantID, err)
239-
}
240-
241-
member, ok := namespace.FindMember(user.ID)
242-
if !ok {
243-
return nil, NewErrNamespaceMemberNotFound(user.ID, nil)
244-
}
245-
246-
if member.Status == models.MemberStatusPending {
247-
return nil, NewErrNamespaceMemberNotFound(user.ID, nil)
248-
}
249-
250-
tenantID = namespace.TenantID
251-
role = member.Role.String()
252-
253-
if user.Preferences.PreferredNamespace != namespace.TenantID {
254-
_ = s.store.UserUpdate(ctx, user.ID, &models.UserChanges{PreferredNamespace: &tenantID})
255-
}
256-
}
257-
258-
claims := authorizer.UserClaims{
259-
ID: user.ID,
260-
Origin: user.Origin.String(),
261-
TenantID: tenantID,
262-
Username: user.Username,
263-
MFA: user.MFA.Enabled,
264-
}
265-
266-
token, err := jwttoken.EncodeUserClaims(claims, s.privKey)
267-
if err != nil {
268-
return nil, NewErrTokenSigned(err)
269-
}
270-
271-
if err := s.AuthCacheToken(ctx, tenantID, user.ID, token); err != nil {
272-
log.WithError(err).Warn("unable to cache the user's auth token")
273-
}
274-
275-
return &models.UserAuthResponse{
276-
ID: user.ID,
277-
Origin: user.Origin.String(),
278-
AuthMethods: user.Preferences.AuthMethods,
279-
User: user.Username,
280-
Name: user.Name,
281-
Email: user.Email,
282-
RecoveryEmail: user.RecoveryEmail,
283-
MFA: user.MFA.Enabled,
284-
Tenant: tenantID,
285-
Role: role,
286-
Token: token,
287-
MaxNamespaces: user.MaxNamespaces,
288-
}, nil
61+
return nil, nil
28962
}
29063

29164
func (s *service) AuthAPIKey(ctx context.Context, key string) (*models.APIKey, error) {
292-
apiKey := new(models.APIKey)
293-
if err := s.cache.Get(ctx, "api-key={"+key+"}", apiKey); err != nil {
294-
return nil, err
295-
}
296-
297-
if apiKey.ID == "" {
298-
keySum := sha256.Sum256([]byte(key))
299-
hashedKey := hex.EncodeToString(keySum[:])
300-
301-
var err error
302-
if apiKey, err = s.store.APIKeyGet(ctx, hashedKey); err != nil {
303-
return nil, NewErrAPIKeyNotFound("", err)
304-
}
305-
}
306-
307-
if !apiKey.IsValid() {
308-
return nil, NewErrAPIKeyInvalid(apiKey.Name)
309-
}
310-
311-
if err := s.cache.Set(ctx, "api-key={"+key+"}", apiKey, 2*time.Minute); err != nil {
312-
log.WithError(err).Info("Unable to set the api-key in cache")
313-
}
314-
315-
return apiKey, nil
65+
return nil, nil
31666
}
31767

31868
func (s *service) AuthPublicKey(ctx context.Context, req requests.PublicKeyAuth) (*models.PublicKeyAuthResponse, error) {
319-
privKey, err := s.store.PrivateKeyGet(ctx, req.Fingerprint)
320-
if err != nil {
321-
return nil, NewErrPublicKeyNotFound(req.Fingerprint, err)
322-
}
323-
324-
block, _ := pem.Decode(privKey.Data)
325-
if block == nil {
326-
return nil, err
327-
}
328-
329-
key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
330-
if err != nil {
331-
return nil, err
332-
}
333-
334-
digest := sha256.Sum256([]byte(req.Data))
335-
signature, err := rsa.SignPKCS1v15(rand.Reader, key, crypto.SHA256, digest[:])
336-
if err != nil {
337-
return nil, err
338-
}
339-
340-
return &models.PublicKeyAuthResponse{
341-
Signature: base64.StdEncoding.EncodeToString(signature),
342-
}, nil
69+
return nil, nil
34370
}
34471

34572
func (s *service) GetUserRole(ctx context.Context, tenantID, userID string) (string, error) {
346-
ns, err := s.store.NamespaceGet(ctx, tenantID)
347-
if err != nil {
348-
return "", err
349-
}
350-
351-
member, ok := ns.FindMember(userID)
352-
if !ok {
353-
return "", NewErrNamespaceMemberNotFound(userID, nil)
354-
}
355-
356-
return member.Role.String(), nil
73+
return "", nil
35774
}
35875

35976
func (s *service) PublicKey() *rsa.PublicKey {

0 commit comments

Comments
 (0)