From a9db2abd98a1171f8d3085c8d903cab8b7ac01c3 Mon Sep 17 00:00:00 2001 From: George Baugh Date: Mon, 19 Aug 2024 13:30:50 -0500 Subject: [PATCH] Add DELETE /users/$USER/tokens Arguments: SHA1 of token. This allows useful workflows like applications which can build and then delete a token for its exclusive use during a session. Requires that a PR to go-gogs-client merges first. Currently you can test that it works by cloning my branch of go-gogs-client, or picking the following commit: https://github.com/teodesian/go-gogs-client/ commit/4488fbb10902b695e7df8c76fa5d6edf5394e0eb and running: go mod init && go mod tidy and finally going back in to this repo to do: go mod edit -replace=github.com/gogs/go-gogs-client=$CLONEDIR go mod tidy go build -o gogs I have tested this endpoint on a local install and verified it works. Beyond that, no automated tests have been added. --- internal/route/api/v1/api.go | 3 +- internal/route/api/v1/user/access_tokens.go | 44 +++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/internal/route/api/v1/api.go b/internal/route/api/v1/api.go index 844012b68..e2ed95a19 100644 --- a/internal/route/api/v1/api.go +++ b/internal/route/api/v1/api.go @@ -189,7 +189,8 @@ func RegisterRoutes(m *macaron.Macaron) { accessTokensHandler := user.NewAccessTokensHandler(user.NewAccessTokensStore()) m.Combo(""). Get(accessTokensHandler.List()). - Post(bind(api.CreateAccessTokenOption{}), accessTokensHandler.Create()) + Post(bind(api.CreateAccessTokenOption{}), accessTokensHandler.Create()). + Delete(bind(api.DeleteAccessTokenOption{}), accessTokensHandler.Delete()) }, reqBasicAuth()) }) }) diff --git a/internal/route/api/v1/user/access_tokens.go b/internal/route/api/v1/user/access_tokens.go index b8c3062a9..05461d12b 100644 --- a/internal/route/api/v1/user/access_tokens.go +++ b/internal/route/api/v1/user/access_tokens.go @@ -59,6 +59,37 @@ func (h *AccessTokensHandler) Create() macaron.Handler { } } +// Deletes the provided token identified by SHA1 +// This prevents anyone from deleting everything in list(), as the identifiers in list() are not the actual SHA1. +func (h *AccessTokensHandler) Delete() macaron.Handler { + //TODO make the latter arg api.DeleteAccessTokenOption + return func(c *context.APIContext, form api.DeleteAccessTokenOption) { + // We need the ID of the token to delete it. + existing, err := h.store.GetBySHA1(c.Req.Context(), form.Sha1); + if err != nil { + if database.IsErrAccessTokenNotExist(err) { + c.ErrorStatus(http.StatusUnprocessableEntity, err) + } else { + c.Error(err, "list tokens in delete prep") + } + return + } + + // We need the User ID and the Token ID to delete it. + err = h.store.DeleteAccessToken(c.Req.Context(), c.User.ID, existing.ID) + if err != nil { + // Always possible that we race, TOCTOU + if database.IsErrAccessTokenNotExist(err) { + c.ErrorStatus(http.StatusUnprocessableEntity, err) + } else { + c.Error(err, "delete access token") + } + return + } + c.JSON(http.StatusOK, &api.AccessToken{Name: existing.Name}) + } +} + // AccessTokensStore is the data layer carrier for user access tokens API // endpoints. This interface is meant to abstract away and limit the exposure of // the underlying data layer to the handler through a thin-wrapper. @@ -69,6 +100,10 @@ type AccessTokensStore interface { CreateAccessToken(ctx gocontext.Context, userID int64, name string) (*database.AccessToken, error) // ListAccessTokens returns all access tokens belongs to given user. ListAccessTokens(ctx gocontext.Context, userID int64) ([]*database.AccessToken, error) + // Get a token by SHA1 (used to find a tok to delete it) + GetBySHA1(ctx gocontext.Context, Sha1 string) (*database.AccessToken, error) + // Delete a given token + DeleteAccessToken(ctx gocontext.Context, userID int64, tokenID int64) error } type accessTokensStore struct{} @@ -86,3 +121,12 @@ func (*accessTokensStore) CreateAccessToken(ctx gocontext.Context, userID int64, func (*accessTokensStore) ListAccessTokens(ctx gocontext.Context, userID int64) ([]*database.AccessToken, error) { return database.Handle.AccessTokens().List(ctx, userID) } + +// Note: the possibility, though remote, of SHA1 collissions could be made far less likely via providing a user as well. +func (*accessTokensStore) GetBySHA1(ctx gocontext.Context, Sha1 string) (*database.AccessToken, error) { + return database.Handle.AccessTokens().GetBySHA1(ctx, Sha1) +} + +func (*accessTokensStore) DeleteAccessToken(ctx gocontext.Context, userID int64, tokenID int64) error { + return database.Handle.AccessTokens().DeleteByID(ctx, userID, tokenID) +}