Skip to content

Commit a578006

Browse files
ashishdevtronkishan789dev
authored and
kishan789dev
committed
fix: deleted api token can be reused if created again with same name (#4978)
* introuddced api token versioning * fix * reverted wire_gen * migration udpated and minor refactor * refactor * moved const from apiToken pkg to user * reverted wire_gen * refactor * reverted wire_gen * concurrency case handled * fix * commented wherever necessary * refactor * fix * refactor * refactor * wip * refactor * added comments and minor refactor * refactor * refactoring * fix * added comments around cyclic import * added few more comments * sql script no updated
1 parent 0b4ea83 commit a578006

10 files changed

+213
-28
lines changed

pkg/apiToken/ApiTokenBean.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package apiToken
2+
3+
import "github.com/golang-jwt/jwt/v4"
4+
5+
type ApiTokenCustomClaims struct {
6+
Email string `json:"email"`
7+
Version string `json:"version"`
8+
jwt.RegisteredClaims
9+
}

pkg/apiToken/ApiTokenRepository.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
package apiToken
1919

2020
import (
21+
"fmt"
2122
"github.com/devtron-labs/devtron/pkg/auth/user/repository"
2223
"github.com/devtron-labs/devtron/pkg/sql"
2324
"github.com/go-pg/pg"
@@ -29,6 +30,7 @@ type ApiToken struct {
2930
Id int `sql:"id,pk"`
3031
UserId int32 `sql:"user_id, notnull"`
3132
Name string `sql:"name, notnull"`
33+
Version int `sql:"version, notnull"`
3234
Description string `sql:"description, notnull"`
3335
ExpireAtInMs int64 `sql:"expire_at_in_ms"`
3436
Token string `sql:"token, notnull"`
@@ -42,6 +44,7 @@ type ApiTokenRepository interface {
4244
FindAllActive() ([]*ApiToken, error)
4345
FindActiveById(id int) (*ApiToken, error)
4446
FindByName(name string) (*ApiToken, error)
47+
UpdateIf(apiToken *ApiToken, previousTokenVersion int) error
4548
}
4649

4750
type ApiTokenRepositoryImpl struct {
@@ -60,6 +63,20 @@ func (impl ApiTokenRepositoryImpl) Update(apiToken *ApiToken) error {
6063
return impl.dbConnection.Update(apiToken)
6164
}
6265

66+
func (impl ApiTokenRepositoryImpl) UpdateIf(apiToken *ApiToken, previousTokenVersion int) error {
67+
res, err := impl.dbConnection.Model(apiToken).
68+
Where("id = ?", apiToken.Id).
69+
Where("version = ?", previousTokenVersion).
70+
Update()
71+
if err != nil {
72+
return err
73+
}
74+
if res.RowsAffected() == 0 {
75+
return fmt.Errorf(TokenVersionMismatch)
76+
}
77+
return nil
78+
}
79+
6380
func (impl ApiTokenRepositoryImpl) FindAllActive() ([]*ApiToken, error) {
6481
var apiTokens []*ApiToken
6582
err := impl.dbConnection.Model(&apiTokens).

pkg/apiToken/ApiTokenService.go

Lines changed: 67 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ package apiToken
2020
import (
2121
"errors"
2222
"fmt"
23+
userBean "github.com/devtron-labs/devtron/pkg/auth/user/bean"
2324
"regexp"
2425
"strconv"
2526
"strings"
@@ -62,14 +63,13 @@ func NewApiTokenServiceImpl(logger *zap.SugaredLogger, apiTokenSecretService Api
6263
}
6364
}
6465

65-
const API_TOKEN_USER_EMAIL_PREFIX = "API-TOKEN:"
66-
6766
var invalidCharsInApiTokenName = regexp.MustCompile("[,\\s]")
6867

69-
type ApiTokenCustomClaims struct {
70-
Email string `json:"email"`
71-
jwt.RegisteredClaims
72-
}
68+
const (
69+
ConcurrentTokenUpdateRequest = "there is an ongoing request for the token with the same name, please try again after some time"
70+
UniqueKeyViolationPgErrorCode = 23505
71+
TokenVersionMismatch = "token version mismatch"
72+
)
7373

7474
func (impl ApiTokenServiceImpl) GetAllApiTokensForWebhook(projectName string, environmentName string, appName string, auth func(token string, projectObject string, envObject string) bool) ([]*openapi.ApiToken, error) {
7575
impl.logger.Info("Getting active api tokens")
@@ -181,11 +181,21 @@ func (impl ApiTokenServiceImpl) CreateApiToken(request *openapi.CreateApiTokenRe
181181

182182
impl.logger.Info(fmt.Sprintf("apiTokenExists : %s", strconv.FormatBool(apiTokenExists)))
183183

184-
// step-2 - Build email
185-
email := fmt.Sprintf("%s%s", API_TOKEN_USER_EMAIL_PREFIX, name)
184+
// step-2 - Build email and version
185+
email := fmt.Sprintf("%s%s", userBean.API_TOKEN_USER_EMAIL_PREFIX, name)
186+
var (
187+
tokenVersion int
188+
previousTokenVersion int
189+
)
190+
if apiTokenExists {
191+
tokenVersion = apiToken.Version + 1
192+
previousTokenVersion = apiToken.Version
193+
} else {
194+
tokenVersion = 1
195+
}
186196

187197
// step-3 - Build token
188-
token, err := impl.createApiJwtToken(email, *request.ExpireAtInMs)
198+
token, err := impl.createApiJwtToken(email, tokenVersion, *request.ExpireAtInMs)
189199
if err != nil {
190200
return nil, err
191201
}
@@ -214,21 +224,37 @@ func (impl ApiTokenServiceImpl) CreateApiToken(request *openapi.CreateApiTokenRe
214224
Description: *request.Description,
215225
ExpireAtInMs: *request.ExpireAtInMs,
216226
Token: token,
227+
Version: tokenVersion,
217228
AuditLog: sql.AuditLog{UpdatedOn: time.Now()},
218229
}
219230
if apiTokenExists {
220231
apiTokenSaveRequest.Id = apiToken.Id
221232
apiTokenSaveRequest.CreatedBy = apiToken.CreatedBy
222233
apiTokenSaveRequest.CreatedOn = apiToken.CreatedOn
223234
apiTokenSaveRequest.UpdatedBy = createdBy
224-
err = impl.apiTokenRepository.Update(apiTokenSaveRequest)
235+
// update api-token only if `previousTokenVersion` is same as version stored in DB
236+
// we are checking this to ensure that two users are not updating the same token at the same time
237+
err = impl.apiTokenRepository.UpdateIf(apiTokenSaveRequest, previousTokenVersion)
225238
} else {
226239
apiTokenSaveRequest.CreatedBy = createdBy
227240
apiTokenSaveRequest.CreatedOn = time.Now()
228241
err = impl.apiTokenRepository.Save(apiTokenSaveRequest)
229242
}
230243
if err != nil {
231244
impl.logger.Errorw("error while saving api-token into DB", "error", err)
245+
// fetching error code from pg error for Unique key violation constraint
246+
// in case of save
247+
pgErr, ok := err.(pg.Error)
248+
if ok {
249+
errCode, conversionErr := strconv.Atoi(pgErr.Field('C'))
250+
if conversionErr == nil && errCode == UniqueKeyViolationPgErrorCode {
251+
return nil, fmt.Errorf(ConcurrentTokenUpdateRequest)
252+
}
253+
}
254+
// in case of update
255+
if errors.Is(err, fmt.Errorf(TokenVersionMismatch)) {
256+
return nil, fmt.Errorf(ConcurrentTokenUpdateRequest)
257+
}
232258
return nil, err
233259
}
234260

@@ -254,22 +280,28 @@ func (impl ApiTokenServiceImpl) UpdateApiToken(apiTokenId int, request *openapi.
254280
return nil, errors.New(fmt.Sprintf("api-token corresponds to apiTokenId '%d' is not found", apiTokenId))
255281
}
256282

283+
previousTokenVersion := apiToken.Version
284+
tokenVersion := apiToken.Version + 1
285+
257286
// step-2 - If expires_at is not same, then token needs to be generated again
258287
if *request.ExpireAtInMs != apiToken.ExpireAtInMs {
259288
// regenerate token
260-
token, err := impl.createApiJwtToken(apiToken.User.EmailId, *request.ExpireAtInMs)
289+
token, err := impl.createApiJwtToken(apiToken.User.EmailId, tokenVersion, *request.ExpireAtInMs)
261290
if err != nil {
262291
return nil, err
263292
}
264293
apiToken.Token = token
294+
apiToken.Version = tokenVersion
265295
}
266296

267297
// step-3 - update in DB
268298
apiToken.Description = *request.Description
269299
apiToken.ExpireAtInMs = *request.ExpireAtInMs
270300
apiToken.UpdatedBy = updatedBy
271301
apiToken.UpdatedOn = time.Now()
272-
err = impl.apiTokenRepository.Update(apiToken)
302+
// update api-token only if `previousTokenVersion` is same as version stored in DB
303+
// we are checking this to ensure that two users are not updating the same token at the same time
304+
err = impl.apiTokenRepository.UpdateIf(apiToken, previousTokenVersion)
273305
if err != nil {
274306
impl.logger.Errorw("error while updating api-token", "apiTokenId", apiTokenId, "error", err)
275307
return nil, err
@@ -322,24 +354,41 @@ func (impl ApiTokenServiceImpl) DeleteApiToken(apiTokenId int, deletedBy int32)
322354

323355
}
324356

325-
func (impl ApiTokenServiceImpl) createApiJwtToken(email string, expireAtInMs int64) (string, error) {
357+
func (impl ApiTokenServiceImpl) createApiJwtToken(email string, tokenVersion int, expireAtInMs int64) (string, error) {
358+
registeredClaims, secretByteArr, err := impl.setRegisteredClaims(expireAtInMs)
359+
if err != nil {
360+
return "", err
361+
}
362+
claims := &ApiTokenCustomClaims{
363+
email,
364+
strconv.Itoa(tokenVersion),
365+
registeredClaims,
366+
}
367+
token, err := impl.generateToken(claims, secretByteArr)
368+
if err != nil {
369+
return "", err
370+
}
371+
return token, nil
372+
}
373+
374+
func (impl ApiTokenServiceImpl) setRegisteredClaims(expireAtInMs int64) (jwt.RegisteredClaims, []byte, error) {
326375
secretByteArr, err := impl.apiTokenSecretService.GetApiTokenSecretByteArr()
327376
if err != nil {
328377
impl.logger.Errorw("error while getting api token secret", "error", err)
329-
return "", err
378+
return jwt.RegisteredClaims{}, secretByteArr, err
330379
}
331380

332381
registeredClaims := jwt.RegisteredClaims{
333382
Issuer: middleware.ApiTokenClaimIssuer,
334383
}
384+
335385
if expireAtInMs > 0 {
336386
registeredClaims.ExpiresAt = jwt.NewNumericDate(time.Unix(expireAtInMs/1000, 0))
337387
}
388+
return registeredClaims, secretByteArr, nil
389+
}
338390

339-
claims := &ApiTokenCustomClaims{
340-
email,
341-
registeredClaims,
342-
}
391+
func (impl ApiTokenServiceImpl) generateToken(claims *ApiTokenCustomClaims, secretByteArr []byte) (string, error) {
343392
unsignedToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
344393
token, err := unsignedToken.SignedString(secretByteArr)
345394
if err != nil {

pkg/auth/user/UserAuthService.go

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import (
3131

3232
"github.com/devtron-labs/authenticator/middleware"
3333
casbin2 "github.com/devtron-labs/devtron/pkg/auth/authorisation/casbin"
34-
bean2 "github.com/devtron-labs/devtron/pkg/auth/user/bean"
34+
userBean "github.com/devtron-labs/devtron/pkg/auth/user/bean"
3535
"github.com/devtron-labs/devtron/pkg/auth/user/repository"
3636
"github.com/go-pg/pg"
3737

@@ -473,7 +473,7 @@ func (impl UserAuthServiceImpl) AuthVerification(r *http.Request) (bool, error)
473473
}
474474
return false, err
475475
}
476-
emailId, err := impl.userService.GetEmailFromToken(token)
476+
emailId, version, err := impl.userService.GetEmailAndVersionFromToken(token)
477477
if err != nil {
478478
impl.logger.Errorw("AuthVerification failed ", "error", err)
479479
return false, err
@@ -488,22 +488,33 @@ func (impl UserAuthServiceImpl) AuthVerification(r *http.Request) (bool, error)
488488
}
489489
return false, err
490490
}
491+
// checking length of version, to ensure backward compatibility as earlier we did not
492+
// have version for api-tokens
493+
// therefore, for tokens without version we will skip the below part
494+
if strings.HasPrefix(emailId, userBean.API_TOKEN_USER_EMAIL_PREFIX) && len(version) > 0 {
495+
err := impl.userService.CheckIfTokenIsValid(emailId, version)
496+
if err != nil {
497+
impl.logger.Errorw("token is not valid", "error", err, "token", token)
498+
return false, err
499+
}
500+
}
491501

492502
//TODO - extends for other purpose
493503
return true, nil
494504
}
505+
495506
func (impl UserAuthServiceImpl) DeleteRoles(entityType string, entityName string, tx *pg.Tx, envIdentifier string, workflowName string) (err error) {
496507
var roleModels []*repository.RoleModel
497508
switch entityType {
498-
case bean2.PROJECT_TYPE:
509+
case userBean.PROJECT_TYPE:
499510
roleModels, err = impl.userAuthRepository.GetRolesForProject(entityName)
500-
case bean2.ENV_TYPE:
511+
case userBean.ENV_TYPE:
501512
roleModels, err = impl.userAuthRepository.GetRolesForEnvironment(entityName, envIdentifier)
502-
case bean2.APP_TYPE:
513+
case userBean.APP_TYPE:
503514
roleModels, err = impl.userAuthRepository.GetRolesForApp(entityName)
504-
case bean2.CHART_GROUP_TYPE:
515+
case userBean.CHART_GROUP_TYPE:
505516
roleModels, err = impl.userAuthRepository.GetRolesForChartGroup(entityName)
506-
case bean2.WorkflowType:
517+
case userBean.WorkflowType:
507518
roleModels, err = impl.userAuthRepository.GetRolesForWorkflow(workflowName, entityName)
508519
}
509520
if err != nil {

0 commit comments

Comments
 (0)