From c5af2c75447965be959c3f1e72483c13c72e46a3 Mon Sep 17 00:00:00 2001 From: Nikolai Rodionov Date: Thu, 14 May 2026 18:10:49 +0200 Subject: [PATCH] WIP: Almost full token controller Signed-off-by: Nikolai Rodionov --- Taskfile.yml | 10 +- api/v1/tokens.go | 133 ++++++++++++++++--- go.mod | 3 +- go.sum | 5 +- internal/controllers/tokens.go | 72 ++++++++-- migrations/20260510174348_tokens_init.up.sql | 2 +- 6 files changed, 192 insertions(+), 33 deletions(-) diff --git a/Taskfile.yml b/Taskfile.yml index 43a0445..eda0991 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -32,7 +32,15 @@ tasks: SOFTPLAYER_DB_CONNECTION_STRING: postgres://softplayer:qwertyu9@localhost:30432/softplayer?sslmode=disable SOFTPLAYER_REDIS_HOST: localhost:30379 cmd: go run main.go server --dev-mode --reflection - + psql: + desc: Connect to the dev database + env: + PGDATABASE: softplayer + PGUSER: softplayer + PGPORT: "30432" + PGHOST: localhost + PGPASSWORD: qwertyu9 + cmd: psql deploy-local-env: desc: Run a kind cluster and deploy deps deps: diff --git a/api/v1/tokens.go b/api/v1/tokens.go index b40c972..01519f3 100644 --- a/api/v1/tokens.go +++ b/api/v1/tokens.go @@ -49,9 +49,10 @@ func (srv *TokensServer) CreateToken(ctx context.Context, in *tokens.CreateToken } tokenData := &controllers.TokenData{ - Name: in.TokenMetadata.GetName(), - UserID: claims.UserID, - Scopes: permissions, + Name: in.TokenMetadata.GetName(), + UserID: claims.UserID, + ExpiresAt: in.TokenMetadata.ExpiresAt.AsTime(), + Scopes: permissions, } token, err := srv.tokenCtrl.Create(ctx, tokenData) @@ -68,13 +69,56 @@ func (srv *TokensServer) CreateToken(ctx context.Context, in *tokens.CreateToken } // ForceTokenExpiration implements [v1.TokensServiceServer]. -func (t *TokensServer) ForceTokenExpiration(context.Context, *tokens.ForceTokenExpirationRequest) (*emptypb.Empty, error) { - return nil, status.Error(codes.Unimplemented, "Method is not implemented") +func (srv *TokensServer) ForceTokenExpiration(ctx context.Context, in *tokens.ForceTokenExpirationRequest) (*emptypb.Empty, error) { + claims, err := srv.authorizationCtrl.ClaimsFromContext(ctx) + if err != nil { + return nil, status.Error(codes.Aborted, "Context is invalid") + } + if claims.UserID == "" { + return nil, status.Error(codes.Aborted, "Context is invalid") + } + + if err := srv.tokenCtrl.ForceExpiration(ctx, in.TokenUuid.GetUuid()); err != nil { + if errors.Is(err, controllers.ErrServerError) { + return nil, status.Error(codes.Internal, "Something is broken on our side") + } + return nil, status.Error(codes.Aborted, "Couldn't create a token") + } + return &emptypb.Empty{}, nil } // GetToken implements [v1.TokensServiceServer]. -func (t *TokensServer) GetToken(context.Context, *tokens.GetTokenRequest) (*tokens.GetTokenResponse, error) { - return nil, status.Error(codes.Unimplemented, "Method is not implemented") +func (srv *TokensServer) GetToken(ctx context.Context, in *tokens.GetTokenRequest) (*tokens.GetTokenResponse, error) { + claims, err := srv.authorizationCtrl.ClaimsFromContext(ctx) + if err != nil { + return nil, status.Error(codes.Aborted, "Context is invalid") + } + if claims.UserID == "" { + return nil, status.Error(codes.Aborted, "Context is invalid") + } + + token, err := srv.tokenCtrl.Get(ctx, in.TokenUuid.Uuid, claims.UserID) + if err != nil { + if errors.Is(err, controllers.ErrServerError) { + return nil, status.Error(codes.Internal, "Something is broken on our side") + } + return nil, status.Error(codes.Aborted, "Couldn't list tokens") + } + + return &tokens.GetTokenResponse{ + TokenUuid: &tokens.TokenUUID{ + Uuid: token.UUID, + }, + TokenMetadata: &tokens.TokenMetadata{ + Name: token.Name, + ExpiresAt: timestamppb.New(token.ExpiresAt), + LastUsedAt: timestamppb.New(token.LastUsedAt), + GeneratedAt: timestamppb.New(token.GeneratedAt), + CreatedAt: timestamppb.New(token.CreatedAt), + RevokedAt: timestamppb.New(token.RevokedAt), + }, + TokenPermissions: &tokens.TokenPermissions{}, + }, nil } // ListTokens implements [v1.TokensServiceServer]. @@ -92,31 +136,86 @@ func (srv *TokensServer) ListTokens(in *emptypb.Empty, stream grpc.ServerStreami if errors.Is(err, controllers.ErrServerError) { return status.Error(codes.Internal, "Something is broken on our side") } - return status.Error(codes.Aborted, "Couldn't create a token") + return status.Error(codes.Aborted, "Couldn't list tokens") } for _, tokenRes := range tokensRes { - stream.Send(&tokens.ListTokensResponse{ + if err := stream.Send(&tokens.ListTokensResponse{ TokenUuid: &tokens.TokenUUID{ Uuid: tokenRes.UUID, }, TokenMetadata: &tokens.TokenMetadata{ - Name: tokenRes.Name, - ExpiresAt: timestamppb.New(tokenRes.ExpiresAt), + Name: tokenRes.Name, + ExpiresAt: timestamppb.New(tokenRes.ExpiresAt), + LastUsedAt: timestamppb.New(tokenRes.LastUsedAt), + GeneratedAt: timestamppb.New(tokenRes.GeneratedAt), + CreatedAt: timestamppb.New(tokenRes.CreatedAt), + RevokedAt: timestamppb.New(tokenRes.RevokedAt), }, - }) + }); err != nil { + return status.Error(codes.Aborted, "Couldn't send data") + } } - return status.Error(codes.Unimplemented, "Method is not implemented") + return nil } // RegenerateToken implements [v1.TokensServiceServer]. -func (t *TokensServer) RegenerateToken(context.Context, *tokens.RegenerateTokenRequest) (*tokens.RegenerateTokenResponse, error) { - return nil, status.Error(codes.Unimplemented, "Method is not implemented") +func (srv *TokensServer) RegenerateToken(ctx context.Context, in *tokens.RegenerateTokenRequest) (*tokens.RegenerateTokenResponse, error) { + claims, err := srv.authorizationCtrl.ClaimsFromContext(ctx) + if err != nil { + return nil, status.Error(codes.Aborted, "Context is invalid") + } + if claims.UserID == "" { + return nil, status.Error(codes.Aborted, "Context is invalid") + } + + tokenVal, err := srv.tokenCtrl.Regenerate(ctx, in.TokenUuid.GetUuid()) + if err != nil { + if errors.Is(err, controllers.ErrServerError) { + return nil, status.Error(codes.Internal, "Something is broken on our side") + } + return nil, status.Error(codes.Aborted, "Couldn't list tokens") + } + return &tokens.RegenerateTokenResponse{ + TokenValue: &tokens.TokenValue{ + Token: tokenVal, + }, + }, nil } // UpdateToken implements [v1.TokensServiceServer]. -func (t *TokensServer) UpdateToken(context.Context, *tokens.UpdateTokenRequest) (*tokens.UpdateTokenResponse, error) { - return nil, status.Error(codes.Unimplemented, "Method is not implemented") +func (srv *TokensServer) UpdateToken(ctx context.Context, in *tokens.UpdateTokenRequest) (*tokens.UpdateTokenResponse, error) { + claims, err := srv.authorizationCtrl.ClaimsFromContext(ctx) + if err != nil { + return nil, status.Error(codes.Aborted, "Context is invalid") + } + if claims.UserID == "" { + return nil, status.Error(codes.Aborted, "Context is invalid") + } + + if in.TokenPermissions == nil { + return nil, status.Error(codes.InvalidArgument, "Permissions must be set") + } + + permissions := map[string][]string{} + for service, methods := range in.TokenPermissions.Permissions { + permissions[service] = methods.GetMethods() + } + tokenData := &controllers.TokenData{ + Name: in.TokenMetadata.Name, + Scopes: permissions, + } + if err := srv.tokenCtrl.Update(ctx, tokenData); err != nil { + if errors.Is(err, controllers.ErrServerError) { + return nil, status.Error(codes.Internal, "Something is broken on our side") + } + return nil, status.Error(codes.Aborted, "Couldn't list tokens") + } + return &tokens.UpdateTokenResponse{ + TokenUuid: &tokens.TokenUUID{}, + TokenMetadata: &tokens.TokenMetadata{}, + TokenPermissions: &tokens.TokenPermissions{}, + }, nil } // ListPermissions implements [v1.TokensServiceServer]. diff --git a/go.mod b/go.mod index b4b23a1..07ed484 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,6 @@ require ( github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 github.com/mattn/go-colorable v0.1.14 - github.com/opentracing/opentracing-go v1.1.0 github.com/redis/go-redis/v9 v9.18.0 github.com/stretchr/testify v1.11.1 go.uber.org/zap v1.27.0 @@ -43,7 +42,7 @@ require ( ) require ( - gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260514115132-f577d4d9c77f + gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260514152254-9c02001a83fd github.com/golang/protobuf v1.5.4 golang.org/x/net v0.49.0 // indirect golang.org/x/sys v0.40.0 // indirect diff --git a/go.sum b/go.sum index 507b266..de6260e 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= -gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260514115132-f577d4d9c77f h1:zNKrOmQPnn+TPV/Zd6vMTYLb2GySSEyt2VawvjP7wb4= -gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260514115132-f577d4d9c77f/go.mod h1:AgOh1lkPHyRgBf3/s1btKcAqke/33LbKYarTD13qeAg= +gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260514152254-9c02001a83fd h1:Qzvr5tAI909J38KNlx3KknzBAgXy5ZJS+0qzvRh/FGI= +gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260514152254-9c02001a83fd/go.mod h1:AgOh1lkPHyRgBf3/s1btKcAqke/33LbKYarTD13qeAg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -117,7 +117,6 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= -github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= diff --git a/internal/controllers/tokens.go b/internal/controllers/tokens.go index e191c0f..9f5b969 100644 --- a/internal/controllers/tokens.go +++ b/internal/controllers/tokens.go @@ -35,7 +35,7 @@ type TokenData struct { Name string UserID string CreatedAt time.Time - LastUserAt time.Time + LastUsedAt time.Time RevokedAt time.Time ExpiresAt time.Time GeneratedAt time.Time @@ -83,12 +83,13 @@ func (ctrl *TokenController) Create(ctx context.Context, data *TokenData) (strin } query := ` - INSERT INTO tokens (uuid, token_hash, user_id, scopes, created_at, generated_at, expires_at) - VALUES ($1, $2, $3, $4, $5, $6, $7)` + INSERT INTO tokens (uuid, description, token_hash, user_id, scopes, created_at, generated_at, expires_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8)` if _, err := ctrl.DB.Query( query, id, + data.Name, tokenHash, data.UserID, scopesJson, @@ -114,12 +115,14 @@ func (ctrl *TokenController) Update(ctx context.Context, data *TokenData) error return ErrServerError } - query := "UPDATE tokens SET name = $1, scopes = $2 WHERE uuid = $3;" + query := "UPDATE tokens SET description = $1, scopes = $2 WHERE uuid = $3;" if _, err := ctrl.DB.Query(query, data.Name, scopesJson, data.UUID); err != nil { log.Error(err, "Couldn't update a token in the database") return ErrServerError } + data, err := ctrl.Get(ctx, , id string, userID string) + return nil } @@ -171,8 +174,42 @@ func (ctrl *TokenController) Regenerate(ctx context.Context, id string) (string, } // Get an existing token data -func (ctrl *TokenController) Get(ctx context.Context, uuid string) error { - return nil +func (ctrl *TokenController) Get(ctx context.Context, id, userID string) (*TokenData, error) { + log := logger.FromContext(ctx).WithValues("uuid", id, "user_id", userID) + log.V(2).Info("Regenerating a token") + query := ` + SELECT uuid, description, generated_at, expires_at, last_used_at, revoked_at, created_at + FROM tokens + WHERE uuid = $1 AND user_id = $2` + token := &TokenData{} + var generatedAt sql.NullTime + var expiresAt sql.NullTime + var revokedAt sql.NullTime + var lastUsedAt sql.NullTime + var createdAt sql.NullTime + + if err := ctrl.DB.QueryRowContext(ctx, query, id, userID).Scan( + &token.UUID, + &token.Name, + &generatedAt, + &expiresAt, + &lastUsedAt, + &revokedAt, + &createdAt, + ); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, err + } + log.Error(err, "Couldn't find a token") + return nil, ErrServerError + } + token.GeneratedAt = generatedAt.Time + token.ExpiresAt = expiresAt.Time + token.RevokedAt = revokedAt.Time + token.LastUsedAt = lastUsedAt.Time + token.CreatedAt = createdAt.Time + + return token, nil } // List all available token @@ -183,7 +220,7 @@ func (ctrl *TokenController) List(ctx context.Context, userID string) ([]TokenDa result := []TokenData{} query := ` - SELECT uuid, name, generated_at, expires_at + SELECT uuid, description, generated_at, expires_at, revoked_at, last_used_at, created_at FROM tokens WHERE user_id = $1` @@ -203,13 +240,30 @@ func (ctrl *TokenController) List(ctx context.Context, userID string) ([]TokenDa for rows.Next() { var t TokenData + var generatedAt sql.NullTime + var expiresAt sql.NullTime + var revokedAt sql.NullTime + var lastUsedAt sql.NullTime + var createdAt sql.NullTime + err := rows.Scan( &t.UUID, &t.Name, - &t.GeneratedAt, - &t.ExpiresAt, + &generatedAt, + &expiresAt, + &revokedAt, + &lastUsedAt, + &createdAt, ) + + t.GeneratedAt = generatedAt.Time + t.ExpiresAt = expiresAt.Time + t.RevokedAt = revokedAt.Time + t.LastUsedAt = lastUsedAt.Time + t.CreatedAt = createdAt.Time + if err != nil { + log.Error(err, "Couldn't write token into a struct") return nil, err } diff --git a/migrations/20260510174348_tokens_init.up.sql b/migrations/20260510174348_tokens_init.up.sql index 0765384..eb4f1e6 100644 --- a/migrations/20260510174348_tokens_init.up.sql +++ b/migrations/20260510174348_tokens_init.up.sql @@ -1,6 +1,6 @@ CREATE TABLE IF NOT EXISTS tokens ( uuid UUID PRIMARY KEY, - name TEXT NOT NULL, + description TEXT NOT NULL, token_hash TEXT NOT NULL, user_id UUID NOT NULL, scopes JSONB NOT NULL DEFAULT '[]',