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 }