code.gitea.io/gitea@v1.22.3/models/auth/access_token.go (about)

     1  // Copyright 2014 The Gogs Authors. All rights reserved.
     2  // Copyright 2019 The Gitea Authors. All rights reserved.
     3  // SPDX-License-Identifier: MIT
     4  
     5  package auth
     6  
     7  import (
     8  	"context"
     9  	"crypto/subtle"
    10  	"encoding/hex"
    11  	"fmt"
    12  	"time"
    13  
    14  	"code.gitea.io/gitea/models/db"
    15  	"code.gitea.io/gitea/modules/setting"
    16  	"code.gitea.io/gitea/modules/timeutil"
    17  	"code.gitea.io/gitea/modules/util"
    18  
    19  	lru "github.com/hashicorp/golang-lru/v2"
    20  	"xorm.io/builder"
    21  )
    22  
    23  // ErrAccessTokenNotExist represents a "AccessTokenNotExist" kind of error.
    24  type ErrAccessTokenNotExist struct {
    25  	Token string
    26  }
    27  
    28  // IsErrAccessTokenNotExist checks if an error is a ErrAccessTokenNotExist.
    29  func IsErrAccessTokenNotExist(err error) bool {
    30  	_, ok := err.(ErrAccessTokenNotExist)
    31  	return ok
    32  }
    33  
    34  func (err ErrAccessTokenNotExist) Error() string {
    35  	return fmt.Sprintf("access token does not exist [sha: %s]", err.Token)
    36  }
    37  
    38  func (err ErrAccessTokenNotExist) Unwrap() error {
    39  	return util.ErrNotExist
    40  }
    41  
    42  // ErrAccessTokenEmpty represents a "AccessTokenEmpty" kind of error.
    43  type ErrAccessTokenEmpty struct{}
    44  
    45  // IsErrAccessTokenEmpty checks if an error is a ErrAccessTokenEmpty.
    46  func IsErrAccessTokenEmpty(err error) bool {
    47  	_, ok := err.(ErrAccessTokenEmpty)
    48  	return ok
    49  }
    50  
    51  func (err ErrAccessTokenEmpty) Error() string {
    52  	return "access token is empty"
    53  }
    54  
    55  func (err ErrAccessTokenEmpty) Unwrap() error {
    56  	return util.ErrInvalidArgument
    57  }
    58  
    59  var successfulAccessTokenCache *lru.Cache[string, any]
    60  
    61  // AccessToken represents a personal access token.
    62  type AccessToken struct {
    63  	ID             int64 `xorm:"pk autoincr"`
    64  	UID            int64 `xorm:"INDEX"`
    65  	Name           string
    66  	Token          string `xorm:"-"`
    67  	TokenHash      string `xorm:"UNIQUE"` // sha256 of token
    68  	TokenSalt      string
    69  	TokenLastEight string `xorm:"INDEX token_last_eight"`
    70  	Scope          AccessTokenScope
    71  
    72  	CreatedUnix       timeutil.TimeStamp `xorm:"INDEX created"`
    73  	UpdatedUnix       timeutil.TimeStamp `xorm:"INDEX updated"`
    74  	HasRecentActivity bool               `xorm:"-"`
    75  	HasUsed           bool               `xorm:"-"`
    76  }
    77  
    78  // AfterLoad is invoked from XORM after setting the values of all fields of this object.
    79  func (t *AccessToken) AfterLoad() {
    80  	t.HasUsed = t.UpdatedUnix > t.CreatedUnix
    81  	t.HasRecentActivity = t.UpdatedUnix.AddDuration(7*24*time.Hour) > timeutil.TimeStampNow()
    82  }
    83  
    84  func init() {
    85  	db.RegisterModel(new(AccessToken), func() error {
    86  		if setting.SuccessfulTokensCacheSize > 0 {
    87  			var err error
    88  			successfulAccessTokenCache, err = lru.New[string, any](setting.SuccessfulTokensCacheSize)
    89  			if err != nil {
    90  				return fmt.Errorf("unable to allocate AccessToken cache: %w", err)
    91  			}
    92  		} else {
    93  			successfulAccessTokenCache = nil
    94  		}
    95  		return nil
    96  	})
    97  }
    98  
    99  // NewAccessToken creates new access token.
   100  func NewAccessToken(ctx context.Context, t *AccessToken) error {
   101  	salt, err := util.CryptoRandomString(10)
   102  	if err != nil {
   103  		return err
   104  	}
   105  	token, err := util.CryptoRandomBytes(20)
   106  	if err != nil {
   107  		return err
   108  	}
   109  	t.TokenSalt = salt
   110  	t.Token = hex.EncodeToString(token)
   111  	t.TokenHash = HashToken(t.Token, t.TokenSalt)
   112  	t.TokenLastEight = t.Token[len(t.Token)-8:]
   113  	_, err = db.GetEngine(ctx).Insert(t)
   114  	return err
   115  }
   116  
   117  // DisplayPublicOnly whether to display this as a public-only token.
   118  func (t *AccessToken) DisplayPublicOnly() bool {
   119  	publicOnly, err := t.Scope.PublicOnly()
   120  	if err != nil {
   121  		return false
   122  	}
   123  	return publicOnly
   124  }
   125  
   126  func getAccessTokenIDFromCache(token string) int64 {
   127  	if successfulAccessTokenCache == nil {
   128  		return 0
   129  	}
   130  	tInterface, ok := successfulAccessTokenCache.Get(token)
   131  	if !ok {
   132  		return 0
   133  	}
   134  	t, ok := tInterface.(int64)
   135  	if !ok {
   136  		return 0
   137  	}
   138  	return t
   139  }
   140  
   141  // GetAccessTokenBySHA returns access token by given token value
   142  func GetAccessTokenBySHA(ctx context.Context, token string) (*AccessToken, error) {
   143  	if token == "" {
   144  		return nil, ErrAccessTokenEmpty{}
   145  	}
   146  	// A token is defined as being SHA1 sum these are 40 hexadecimal bytes long
   147  	if len(token) != 40 {
   148  		return nil, ErrAccessTokenNotExist{token}
   149  	}
   150  	for _, x := range []byte(token) {
   151  		if x < '0' || (x > '9' && x < 'a') || x > 'f' {
   152  			return nil, ErrAccessTokenNotExist{token}
   153  		}
   154  	}
   155  
   156  	lastEight := token[len(token)-8:]
   157  
   158  	if id := getAccessTokenIDFromCache(token); id > 0 {
   159  		accessToken := &AccessToken{
   160  			TokenLastEight: lastEight,
   161  		}
   162  		// Re-get the token from the db in case it has been deleted in the intervening period
   163  		has, err := db.GetEngine(ctx).ID(id).Get(accessToken)
   164  		if err != nil {
   165  			return nil, err
   166  		}
   167  		if has {
   168  			return accessToken, nil
   169  		}
   170  		successfulAccessTokenCache.Remove(token)
   171  	}
   172  
   173  	var tokens []AccessToken
   174  	err := db.GetEngine(ctx).Table(&AccessToken{}).Where("token_last_eight = ?", lastEight).Find(&tokens)
   175  	if err != nil {
   176  		return nil, err
   177  	} else if len(tokens) == 0 {
   178  		return nil, ErrAccessTokenNotExist{token}
   179  	}
   180  
   181  	for _, t := range tokens {
   182  		tempHash := HashToken(token, t.TokenSalt)
   183  		if subtle.ConstantTimeCompare([]byte(t.TokenHash), []byte(tempHash)) == 1 {
   184  			if successfulAccessTokenCache != nil {
   185  				successfulAccessTokenCache.Add(token, t.ID)
   186  			}
   187  			return &t, nil
   188  		}
   189  	}
   190  	return nil, ErrAccessTokenNotExist{token}
   191  }
   192  
   193  // AccessTokenByNameExists checks if a token name has been used already by a user.
   194  func AccessTokenByNameExists(ctx context.Context, token *AccessToken) (bool, error) {
   195  	return db.GetEngine(ctx).Table("access_token").Where("name = ?", token.Name).And("uid = ?", token.UID).Exist()
   196  }
   197  
   198  // ListAccessTokensOptions contain filter options
   199  type ListAccessTokensOptions struct {
   200  	db.ListOptions
   201  	Name   string
   202  	UserID int64
   203  }
   204  
   205  func (opts ListAccessTokensOptions) ToConds() builder.Cond {
   206  	cond := builder.NewCond()
   207  	// user id is required, otherwise it will return all result which maybe a possible bug
   208  	cond = cond.And(builder.Eq{"uid": opts.UserID})
   209  	if len(opts.Name) > 0 {
   210  		cond = cond.And(builder.Eq{"name": opts.Name})
   211  	}
   212  	return cond
   213  }
   214  
   215  func (opts ListAccessTokensOptions) ToOrders() string {
   216  	return "created_unix DESC"
   217  }
   218  
   219  // UpdateAccessToken updates information of access token.
   220  func UpdateAccessToken(ctx context.Context, t *AccessToken) error {
   221  	_, err := db.GetEngine(ctx).ID(t.ID).AllCols().Update(t)
   222  	return err
   223  }
   224  
   225  // DeleteAccessTokenByID deletes access token by given ID.
   226  func DeleteAccessTokenByID(ctx context.Context, id, userID int64) error {
   227  	cnt, err := db.GetEngine(ctx).ID(id).Delete(&AccessToken{
   228  		UID: userID,
   229  	})
   230  	if err != nil {
   231  		return err
   232  	} else if cnt != 1 {
   233  		return ErrAccessTokenNotExist{}
   234  	}
   235  	return nil
   236  }