From e96ec08a869f303f90fed446389e9324d88f7701 Mon Sep 17 00:00:00 2001 From: Nikolai Rodionov Date: Thu, 14 May 2026 12:26:03 +0200 Subject: [PATCH] List possible services and filter unavailable Signed-off-by: Nikolai Rodionov --- api/v1/test.go | 20 ++++++++ api/v1/tokens.go | 91 ++++++++++++++++++++++++++++++++++ cmd/server.go | 36 ++++++++++++-- go.mod | 2 +- go.sum | 4 +- internal/controllers/tokens.go | 54 +++++++++++++++++++- 6 files changed, 197 insertions(+), 10 deletions(-) diff --git a/api/v1/test.go b/api/v1/test.go index c3739b0..94617f9 100644 --- a/api/v1/test.go +++ b/api/v1/test.go @@ -4,6 +4,8 @@ import ( "context" test "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/test/v1" + "google.golang.org/grpc" + "google.golang.org/protobuf/types/known/emptypb" ) func NewTestServer() *TestServer { @@ -18,6 +20,15 @@ func (t *TestServer) Pong(ctx context.Context, in *test.PongRequest) (*test.Pong return &test.PongResponse{}, nil } +func (t *TestServer) PongStream(in *emptypb.Empty, stream grpc.ServerStreamingServer[test.PongStreamResponse]) error { + for i := 0; i < 10; i++ { + if err := stream.Send(&test.PongStreamResponse{Dummy: "test"}); err != nil { + return err + } + } + return nil +} + func NewPublicTestServer() *PublicTestServer { return &PublicTestServer{} } @@ -29,3 +40,12 @@ type PublicTestServer struct { func (t *PublicTestServer) Ping(ctx context.Context, in *test.PingRequest) (*test.PingResponse, error) { return &test.PingResponse{}, nil } + +func (t *PublicTestServer) PingStream(in *emptypb.Empty, stream grpc.ServerStreamingServer[test.PingStreamResponse]) error { + for i := 0; i < 10; i++ { + if err := stream.Send(&test.PingStreamResponse{Dummy: "test"}); err != nil { + return err + } + } + return nil +} diff --git a/api/v1/tokens.go b/api/v1/tokens.go index 4c115eb..dcd0e99 100644 --- a/api/v1/tokens.go +++ b/api/v1/tokens.go @@ -1,9 +1,100 @@ package v1 import ( + "context" + + "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/controllers" tokens "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/tokens/v1" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" ) +// var _ tokens.TokensServiceServer = (*TokensServer)(nil) type TokensServer struct { tokens.UnimplementedTokensServiceServer + tokenCtrl *controllers.TokenController + authorizationCtrl *controllers.AuthController +} + +func NewTokensServer( + tokenCtrl *controllers.TokenController, + authorizationCtrl *controllers.AuthController, +) *TokensServer { + return &TokensServer{ + tokenCtrl: tokenCtrl, + authorizationCtrl: authorizationCtrl, + } +} + +// CreateToken implements [v1.TokensServiceServer]. +func (srv *TokensServer) CreateToken(ctx context.Context, in *tokens.CreateTokenRequest) (*tokens.CreateTokenResponse, 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") + } + tokenData := &controllers.TokenData{ + Name: in.TokenMetadata.GetName(), + UserID: claims.UserID, + Scopes: &controllers.Scopes{}, + } + + token, err := srv.tokenCtrl.Create(ctx, tokenData) + if err != nil { + return nil, err + } + + return &tokens.CreateTokenResponse{ + TokenUuid: &tokens.TokenUUID{}, + TokenMetadata: &tokens.TokenMetadata{}, + TokenPermissions: &tokens.TokenPermissions{}, + TokenValue: &tokens.TokenValue{Token: token}, + }, status.Error(codes.Unimplemented, "Method is not implemented") +} + +// 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") +} + +// 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") +} + +// ListTokens implements [v1.TokensServiceServer]. +func (t *TokensServer) ListTokens(*emptypb.Empty, grpc.ServerStreamingServer[tokens.ListTokensResponse]) error { + return status.Error(codes.Unimplemented, "Method is not implemented") +} + +// 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") +} + +// 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") +} + +// ListPermissions implements [v1.TokensServiceServer]. +func (srv *TokensServer) ListPermissions(in *emptypb.Empty, stream grpc.ServerStreamingServer[tokens.ListPermissionsResponse]) error { + data := srv.tokenCtrl.ListPermissions(stream.Context()) + for key, data := range data { + result := &tokens.ListPermissionsResponse{ + Permissions: &tokens.Permissions{ + AvailabiePermissions: map[string]*tokens.MethodList{ + key: {Methods: data}, + }, + }, + } + if err := stream.Send(result); err != nil { + return status.Error(codes.Aborted, "Couldn't send data to the client") + } + } + return nil } diff --git a/cmd/server.go b/cmd/server.go index 9b9dfc9..a4eb2b1 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -13,6 +13,7 @@ import ( "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/logger" accounts "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/accounts/v1" test "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/test/v1" + tokens "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/tokens/v1" grpc_zap "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap" "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors" "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/auth" @@ -82,13 +83,24 @@ func (cmd *Server) Run(ctx context.Context) error { selector.MatchFunc(selectorRequireAuth), ), ), - grpc.StreamInterceptor(grpc_zap.StreamServerInterceptor(logger.SetupLogger("info"))), + grpc.ChainStreamInterceptor( + grpc_zap.StreamServerInterceptor(logger.SetupLogger("info")), + selector.StreamServerInterceptor( + auth.StreamServerInterceptor(authInterceptor.AuthInterceptorFN), + selector.MatchFunc(selectorRequireAuth), + ), + ), ) if cmd.Reflection { reflection.Register(grpcServer) } + tokenCtrl := &controllers.TokenController{ + DB: db, + HashCost: cmd.HashCost, + } + accountCtrl := &controllers.AccountController{ HashCost: cmd.HashCost, DB: db, @@ -99,10 +111,17 @@ func (cmd *Server) Run(ctx context.Context) error { Redis: rdb, } - accounts.RegisterPublicAccountsServiceServer(grpcServer, v1.NewPublicAccountServer(accountCtrl, authInterceptor)) + // Services that should be accessible for tokens should go here accounts.RegisterAccountsServiceServer(grpcServer, v1.NewAccountServer(accountCtrl, authInterceptor)) test.RegisterTestServiceServer(grpcServer, v1.NewTestServer()) test.RegisterPublicTestServiceServer(grpcServer, v1.NewPublicTestServer()) + tokens.RegisterTokensServiceServer(grpcServer, v1.NewTokensServer(tokenCtrl, authInterceptor)) + accounts.RegisterPublicAccountsServiceServer(grpcServer, v1.NewPublicAccountServer(accountCtrl, authInterceptor)) + + info := grpcServer.GetServiceInfo() + tokenCtrl.SetGRPCInfo(info) + tokenCtrl.SetRules() + if err := grpcServer.Serve(lis); err != nil { return err } @@ -116,8 +135,15 @@ func selectorRequireAuth(ctx context.Context, callMeta interceptors.CallMeta) bo if len(serviceParts) == 0 { return false } - serviceName := serviceParts[len(serviceParts)-1] - fmt.Println(serviceName) - return !strings.HasPrefix(serviceName, "Public") + + if strings.HasPrefix(serviceName, "Public") { + return false + } + + if strings.Contains(serviceName, "ServerReflection") { + return false + } + + return true } diff --git a/go.mod b/go.mod index 0c96d25..4da5701 100644 --- a/go.mod +++ b/go.mod @@ -42,7 +42,7 @@ require ( ) require ( - gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260511094640-0f4959475dc9 + gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260514095622-3ce39b865e5a 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 2045178..606e00d 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-20260511094640-0f4959475dc9 h1:gVB4z3qZXriL8xfJnY8hEqiBTGHGeeWO5E3GY/7aNRM= -gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260511094640-0f4959475dc9/go.mod h1:AgOh1lkPHyRgBf3/s1btKcAqke/33LbKYarTD13qeAg= +gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260514095622-3ce39b865e5a h1:F21MJw0xsiZf3cj4D+n8JPqkX38XlY+xFju2gQkC9eA= +gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260514095622-3ce39b865e5a/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= diff --git a/internal/controllers/tokens.go b/internal/controllers/tokens.go index 8a64e37..fe8252d 100644 --- a/internal/controllers/tokens.go +++ b/internal/controllers/tokens.go @@ -4,19 +4,26 @@ import ( "context" "database/sql" "errors" + "regexp" "time" "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/hash" "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/logger" "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/token" "github.com/google/uuid" + "google.golang.org/grpc" ) type TokenController struct { - DB *sql.DB - HashCost int16 + DB *sql.DB + HashCost int16 + ServiceInfo map[string]grpc.ServiceInfo + rules []rule } +// Services that are not available for tokens +var DisabledServicesRegex = []string{".*Accounts.*", ".*Tokens.*"} + // Errors var ( ErrServerError = errors.New("internal server error") @@ -35,6 +42,20 @@ type TokenData struct { type Scopes struct{} +// Set the grpc info, must happen after all the service are initialized +func (ctrl *TokenController) SetGRPCInfo(info map[string]grpc.ServiceInfo) { + ctrl.ServiceInfo = info +} + +func (ctrl *TokenController) SetRules() { + rules := []rule{ + {re: regexp.MustCompile(`.*Tokens.*`)}, + {re: regexp.MustCompile(`.*Accounts.*`)}, + {re: regexp.MustCompile(`.*Reflection.*`)}, + } + ctrl.rules = rules +} + // Create a new token, store its hash in the database and return the token value func (ctrl *TokenController) Create(ctx context.Context, data *TokenData) (string, error) { id := uuid.NewString() @@ -86,3 +107,32 @@ func (ctrl *TokenController) Get(ctx context.Context, uuid string) error { func (ctrl *TokenController) List(ctx context.Context) error { return nil } + +// Lis all available permissions +func (ctrl *TokenController) ListPermissions(ctx context.Context) (result map[string][]string) { + result = map[string][]string{} + for key, val := range ctrl.ServiceInfo { + if shouldSkip(key, ctrl.rules) { + continue + } + var services []string + for _, svc := range val.Methods { + services = append(services, svc.Name) + } + result[key] = services + } + return +} + +type rule struct { + re *regexp.Regexp +} + +func shouldSkip(s string, rules []rule) bool { + for _, r := range rules { + if r.re.MatchString(s) { + return true + } + } + return false +}