@@ -20,6 +20,7 @@ package apiToken
2020import (
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-
6766var 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
7474func (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 {
0 commit comments