diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 49d8ff10..7ee31a1f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,9 +55,6 @@ jobs: run: | make gen - - name: Generate Go code - run: make gen-code - - name: Run tests run: | go clean -testcache diff --git a/CLAUDE.md b/CLAUDE.md index 3f5db3d3..5545baf7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,8 +7,6 @@ Shared Go module (`github.com/percona/platform`) providing: - **Generated Go code** from those protos (under `gen/`, committed to the repo) - **Shared Go libraries** consumed across all platform services (under `pkg/`) -The sibling repo `github.com/percona/saas` is a public mirror — `make saas` extracts a subset of this repo there, rewriting all import paths. - --- ## Directory structure @@ -46,14 +44,12 @@ Mirrors `api/` structure. Regenerate with `make gen`. make init # Build all dev tools into bin/ (run once after checkout) make gen # Regenerate all protobuf code (rm -rf gen, buf generate, format, breaking check) make descriptors # Update platform.bin used for breaking-change detection (run before make gen when removing protos) -make gen-code # go generate + go install make format # gofumpt + goimports + buf format make check # go-consistent + golangci-lint make test # go test -race ./... make test-cover # Per-package coverage → cover.out make test-crosscover # Cross-package coverage → crosscover.out make swagger-ui # Serve Swagger docs locally on :8081 (Docker required) -make saas # Extract public subset to ../saas with import path rewrite ``` --- @@ -92,5 +88,5 @@ Generated code is **committed to the repo**. ## CI -- **ci.yml**: on push to `main` and all PRs — runs `make gen`, `make gen-code`, cross-coverage tests, then `make format` + `make check` with reviewdog annotations on PRs +- **ci.yml**: on push to `main` and all PRs — runs `make gen`, cross-coverage tests, then `make format` + `make check` with reviewdog annotations on PRs - **Verification step**: CI asserts no uncommitted source changes after gen (go.sum tidying is the only allowed diff) diff --git a/Makefile b/Makefile index f338e0c4..a65b0654 100644 --- a/Makefile +++ b/Makefile @@ -17,10 +17,6 @@ gen: ## Format, check, and generate code u make format bin/buf breaking --against platform.bin api -gen-code: ## Generate code - go generate ./... - go install ./... - swagger-ui: ## Serve API documentation with SwaggerUI docker run -p 8081:8080 -e URLS='[ \ {name:"telemetryd", url:"/gen/telemetry/reporter/reporter_api.swagger.json"}, \ @@ -47,7 +43,4 @@ test-crosscover: ## Run tests and collect cross-packag descriptors: ## Update files used for breaking changes detection bin/buf build -o platform.bin --as-file-descriptor-set api -saas: ## Extract public APIs and generated files into ../saas - go run post-processing.go -project saas - .PHONY: $(MAKECMDGOALS) diff --git a/pkg/servers/auth_interceptor.go b/pkg/servers/auth_interceptor.go index 16509933..8d5acb4a 100644 --- a/pkg/servers/auth_interceptor.go +++ b/pkg/servers/auth_interceptor.go @@ -1,27 +1,18 @@ package servers import ( - "context" - "fmt" "net/textproto" "strconv" "strings" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" - "github.com/pkg/errors" - "go.uber.org/zap" - "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" - "google.golang.org/grpc/status" - "github.com/percona/platform/pkg/logger" "github.com/percona/platform/pkg/rdata" "github.com/percona/platform/pkg/tracing" ) -type authMethodType int - // Headers set by proxy. const ( // AuthUsernameHeader Percona Account username that is used for authentication. @@ -66,22 +57,8 @@ const ( // AuthErrorHeader gRPC error message, if code is not codes.OK. AuthErrorHeader = "Auth-Error" - - // Auth methods types. - - // Method doesn't require authentication. - authMethodNoAuth authMethodType = iota - - // Method may use authentication data if it exists. - // Anonymous calls of this method are allowed as well. - authMethodMayUseAuth - - // Method require authentication otherwise it will be rejected. - authMethodRequireAuth ) -var errAuthenticationFail = status.Error(codes.Unauthenticated, "Authentication fail.") - // AuthMetadata returns auth headers. func AuthMetadata(r *rdata.RequestData) metadata.MD { return metadata.Pairs( @@ -119,323 +96,3 @@ func PerconaHeaderMatcher(key string) (string, bool) { func perconaAuthHeadersMatcher(key string) bool { return strings.HasPrefix(key, "Auth-") } - -func unaryAuthInterceptor(noAuthMethods, mayUseAuthMethods []string) grpc.UnaryServerInterceptor { //nolint:cyclop, funlen - noAuthMethodsSet := make(map[string]struct{}, len(noAuthMethods)) - mayUseAuthMethodsSet := make(map[string]struct{}, len(mayUseAuthMethods)) - - for _, m := range noAuthMethods { - noAuthMethodsSet[m] = struct{}{} - } - - for _, m := range mayUseAuthMethods { - if _, ok := noAuthMethodsSet[m]; ok { - panic(fmt.Sprintf("method %s can't be listed in NoAuthMethods and MayUseAuthMethods simultaneously", m)) - } - mayUseAuthMethodsSet[m] = struct{}{} - } - - return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { - l := logger.GetLoggerFromContext(ctx) - - md, ok := metadata.FromIncomingContext(ctx) - if !ok { - l.Error("No metadata in incoming request. Rejecting request.") - return nil, errAuthenticationFail - } - l.Debug("Received metadata", zap.Any("metadata", md)) - - // Check authentication error before checking if methods requires authentication at all: - // * if Authorization header is absent, Auth Service returns OK; - // * but if Authorization header is present, it should be valid. - if err := handleAuthProxyError(md, l); err != nil { - l.Error("Incoming request is unauthenticated. Rejecting request.") - return nil, err - } - - authData := new(rdata.RequestData) - var err error - - switch getAuthMethodType(noAuthMethodsSet, mayUseAuthMethodsSet, info.FullMethod) { - case authMethodRequireAuth: - // Request must be authenticated. - authData, err = getAuthData(md) - if err != nil { - l.Error("Can't extract auth data from incoming request. Rejecting request.", zap.Error(err)) - return nil, errAuthenticationFail - } - ctx = rdata.AddToContext(ctx, authData) - case authMethodMayUseAuth, authMethodNoAuth: - // In case auth data exist add it to context. - tmpAuthData, err := getAuthData(md) - if err == nil { - authData = tmpAuthData - ctx = rdata.AddToContext(ctx, authData) - } - default: - // Do not try to extract auth data from incoming context. - } - - // Add logger with userID/appID attributes to context. - // This logger will be extracted from context and used later by service layers. - zapUserID := zap.Skip() - if len(authData.UserID) != 0 { - zapUserID = zap.String(logger.UserIDAttr, authData.UserID) - } - - zapAppID := zap.Skip() - if len(authData.AppID) != 0 { - zapAppID = zap.String(logger.AppIDAttr, authData.AppID) - } - - zapHook := zap.Skip() - if authData.Hook { - zapHook = zap.Bool(logger.HookAttr, authData.Hook) - } - - ctx = logger.GetContextWithLogger(ctx, l.With(zapUserID, zapAppID, zapHook)) - return handler(ctx, req) - } -} - -func streamAuthInterceptor(noAuthMethods, mayUseAuthMethods []string) grpc.StreamServerInterceptor { //nolint:cyclop, funlen - noAuthMethodsSet := make(map[string]struct{}, len(noAuthMethods)) - mayUseAuthMethodsSet := make(map[string]struct{}, len(mayUseAuthMethods)) - - for _, m := range noAuthMethods { - noAuthMethodsSet[m] = struct{}{} - } - - for _, m := range mayUseAuthMethods { - if _, ok := noAuthMethodsSet[m]; ok { - panic(fmt.Sprintf("method %s can't be listed in NoAuthMethods and MayUseAuthMethods simultaneously", m)) - } - mayUseAuthMethodsSet[m] = struct{}{} - } - - return func(_ interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { - ctx := ss.Context() - l := logger.GetLoggerFromContext(ctx) - - md, ok := metadata.FromIncomingContext(ctx) - if !ok { - l.Error("No metadata in incoming request. Rejecting request.") - return errAuthenticationFail - } - l.Debug("Received metadata", zap.Any("metadata", md)) - - // Check authentication error before checking if methods requires authentication at all: - // * if Authorization header is absent, Auth Service returns OK; - // * but if Authorization header is present, it should be valid. - if err := handleAuthProxyError(md, l); err != nil { - l.Error("Incoming request is unauthenticated. Rejecting request.") - return err - } - - authData := new(rdata.RequestData) - var err error - - switch getAuthMethodType(noAuthMethodsSet, mayUseAuthMethodsSet, info.FullMethod) { - case authMethodRequireAuth: - // Request must be authenticated. - authData, err = getAuthData(md) - if err != nil { - l.Error("Can't extract auth data from incoming request. Rejecting request.", zap.Error(err)) - return errAuthenticationFail - } - ctx = rdata.AddToContext(ctx, authData) - case authMethodMayUseAuth, authMethodNoAuth: - // In case auth data exist add it to context. - tmpAuthData, err := getAuthData(md) - if err == nil { - authData = tmpAuthData - ctx = rdata.AddToContext(ctx, authData) - } - default: - // Do not try to extract auth data from incoming context. - } - - // Add logger with userID/appID attributes to context. - // This logger will be extracted from context and used later by service layers. - zapUserID := zap.Skip() - if len(authData.UserID) != 0 { - zapUserID = zap.String(logger.UserIDAttr, authData.UserID) - } - - zapAppID := zap.Skip() - if len(authData.AppID) != 0 { - zapAppID = zap.String(logger.AppIDAttr, authData.AppID) - } - - ctx = logger.GetContextWithLogger(ctx, l.With(zapUserID, zapAppID)) - return handler(ctx, ss) - } -} - -func getAuthMethodType(noAuthMethodsSet, mayUseAuthMethodsSet map[string]struct{}, m string) authMethodType { - if _, ok := noAuthMethodsSet[m]; ok { - return authMethodNoAuth - } - - if _, ok := mayUseAuthMethodsSet[m]; ok { - return authMethodMayUseAuth - } - return authMethodRequireAuth -} - -// handleAuthProxyError checks authentication status and message forwarded from proxy -// and returns proper response to user in case of any problem. -func handleAuthProxyError(md metadata.MD, l *zap.Logger) error { - authStatus, err := getAuthStatusFromMetadata(md) - if err != nil { - l.Error("Failed to get auth status from request metadata.", zap.Error(err)) - return errAuthenticationFail - } - - if authStatus != codes.OK { - authError, err := getAuthErrorFromMetadata(md) - if err != nil { - l.Error("Failed to extract auth error from incoming request.", zap.Error(err)) - return errAuthenticationFail - } - return status.Error(authStatus, authError) - } - - return nil -} - -// getAuthData extracts user email and session id from request metadata. -func getAuthData(md metadata.MD) (*rdata.RequestData, error) { //nolint: funlen, cyclop - username, err := getStringFromMetadata(md, AuthUsernameHeader) - if err != nil { - return nil, errors.Wrapf(err, "failed to get %s from request metadata", AuthUsernameHeader) - } - - userID, err := getStringFromMetadata(md, AuthUserIDHeader) - if err != nil { - return nil, errors.Wrapf(err, "failed to get %s from request metadata", AuthUserIDHeader) - } - - appID, err := getStringFromMetadata(md, AuthAppIDHeader) - if err != nil { - return nil, errors.Wrapf(err, "failed to get %s from request metadata", AuthAppIDHeader) - } - - isPortalSuperAdmin, err := getBoolFromMetadata(md, AuthSuperAdminHeader) - if err != nil { - return nil, errors.Wrapf(err, "failed to get %s from request metadata", AuthSuperAdminHeader) - } - - portalOrgID, err := getStringFromMetadata(md, AuthPortalOrgIDHeader) - if err != nil { - return nil, errors.Wrapf(err, "failed to get %s from request metadata", AuthPortalOrgIDHeader) - } - - authToken, err := getStringFromMetadata(md, AuthTokenHeader) - if err != nil { - return nil, errors.Wrapf(err, "failed to get %s from request metadata", AuthTokenHeader) - } - - isHook, err := getBoolFromMetadata(md, AuthHook) - if err != nil { - return nil, errors.Wrapf(err, "failed to get %s from request metadata", AuthHook) - } - - hookVerification, err := getStringFromMetadata(md, OktaVerificationHeader) - if err != nil { - return nil, errors.Wrapf(err, "failed to get %s from request metadata", OktaVerificationHeader) - } - - // Keep for backward compatibility. - email, err := getStringFromMetadata(md, AuthEmailHeader) - if err != nil { - return nil, errors.Wrapf(err, "failed to get %s from request metadata", AuthEmailHeader) - } - - sessionID, err := getStringFromMetadata(md, AuthSessionHeader) - if err != nil { - return nil, errors.Wrapf(err, "failed to get %s from request metadata", AuthSessionHeader) - } - - // The following cases possible: - // - Auth-Username header is not empty - it means this incoming request we are processing now - // is from real user (browser). - // - Auth-Portal-Org-ID header is not empty - it means this incoming request we are processing now - // is from PMM Server (machine-to-machine communication). - // - Auth-Hook is true - it means that the request is from a Hook method - // Authorized incoming request must have the isHook=true or contain one of: username, portalOrgID, sessionID - if !isHook && len(username) == 0 && len(portalOrgID) == 0 && len(sessionID) == 0 { - return nil, fmt.Errorf("at least one of the auth headers [%s,%s,%s,%s] must be provided", AuthUsernameHeader, AuthPortalOrgIDHeader, AuthSessionHeader, AuthHook) - } - - return &rdata.RequestData{ - Username: username, - UserID: userID, - AppID: appID, - IsPortalSuperAdmin: isPortalSuperAdmin, - PortalOrgID: portalOrgID, - AuthToken: authToken, - UserEmail: email, - SessionID: sessionID, - Hook: isHook, - HookVerification: hookVerification, - }, nil -} - -// getAuthStatusFromMetadata extracts auth status set by proxy from metadata. -func getAuthStatusFromMetadata(md metadata.MD) (codes.Code, error) { - header := md.Get(AuthStatusHeader) - if len(header) != 1 { - return 0, fmt.Errorf("expect exactly one auth status header, got: %d", len(header)) - } - - c, err := strconv.Atoi(header[0]) - if err != nil { - return 0, errors.Wrap(err, "failed to parse auth status code") - } - - return codes.Code(c), nil //nolint: gosec -} - -// getAuthErrorFromMetadata extracts auth error message set by proxy from metadata. -func getAuthErrorFromMetadata(md metadata.MD) (string, error) { - header := md.Get(AuthErrorHeader) - if len(header) != 1 { - return "", fmt.Errorf("expect exactly one %s header, got: %d", AuthErrorHeader, len(header)) - } - - return header[0], nil -} - -// getStringFromMetadata extracts string key set by proxy from metadata. -func getStringFromMetadata(md metadata.MD, key string) (string, error) { - header := md.Get(key) - if len(header) > 1 { - return "", fmt.Errorf("expect at most one %s header, got: %d", key, len(header)) - } - - if len(header) == 0 { - return "", nil - } - - return header[0], nil -} - -// getBoolFromMetadata extracts bool key set by proxy from metadata. -func getBoolFromMetadata(md metadata.MD, key string) (bool, error) { - header := md.Get(key) - if len(header) > 1 { - return false, fmt.Errorf("expect at most one %s header, got: %d", key, len(header)) - } - - if len(header) == 0 { - return false, nil - } - - v, err := strconv.ParseBool(header[0]) - if err != nil { - return false, errors.Wrapf(err, "failed to parse %s header", key) - } - - return v, nil -} diff --git a/post-processing.go b/post-processing.go deleted file mode 100644 index 5f5840b7..00000000 --- a/post-processing.go +++ /dev/null @@ -1,194 +0,0 @@ -//go:build ignore -// +build ignore - -package main - -import ( - "bytes" - "encoding/json" - "flag" - "fmt" - "io" - "io/ioutil" - "log" - "os" - "os/exec" - "path/filepath" - "regexp" - "strings" -) - -const ( - platformRepo = "github.com/percona/platform" - saasRepo = "github.com/percona/saas" - saasRoot = "../saas" -) - -var generatedImportRe = regexp.MustCompile(`(?mi)[\n]^.*github_com_mwitkow.*$`) - -func saasFilePatch(content []byte) []byte { - return bytes.Replace(content, []byte(platformRepo), []byte(saasRepo), -1) -} - -// copyAndPatchFile copies a file src to dst, applying a specified patch function -func copyAndPatchFile(src, dst string, patchFunc func([]byte) []byte) error { - b, err := ioutil.ReadFile(src) //nolint:gosec - if err != nil { - return err - } - - b = patchFunc(b) - - if err = os.MkdirAll(filepath.Dir(dst), 0o755); err != nil { //nolint:gosec - return err - } - - return ioutil.WriteFile(dst, b, 0o644) -} - -// runInDir runs command name with args in dir and returns stdout. -func runInDir(dir, name string, args ...string) ([]byte, error) { - cmd := exec.Command(name, args...) //nolint:gosec - cmd.Dir = dir - cmd.Stderr = os.Stderr - log.Print(strings.Join(cmd.Args, " ")) - return cmd.Output() -} - -func removeDirs(root string, directories ...string) { - for _, d := range directories { - path := filepath.Join(root, d) - log.Printf("Removing %s ...", path) - if err := os.RemoveAll(path); err != nil { - log.Fatal(err) - } - } -} - -// makeProcessDirsFunc returns a function that applies patch function to included files and copies them to the root directory. -func makeProcessDirsFunc(root string, patchFunc func([]byte) []byte, includeFiles []string, excludeFiles []string) func(string, os.FileInfo, error) error { - return func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - - var copy bool - for _, s := range includeFiles { - if strings.Contains(path, "internal") { - // TODO: Improve internal packages handling - panic("internal packages should not be copied to saas repo") - } - if strings.HasSuffix(path, s) { - copy = true - } - } - for _, s := range excludeFiles { - if strings.HasSuffix(path, s) { - copy = false - } - } - - if !copy { - return nil - } - - dst := filepath.Join(root, path) - log.Printf(" %s -> %s", path, dst) - return copyAndPatchFile(path, dst, patchFunc) - } -} - -func walk(processDirsFunc func(string, os.FileInfo, error) error, directories ...string) { - log.Print("Copying and patching files:") - for _, src := range directories { - err := filepath.Walk(src, processDirsFunc) - if err != nil { - log.Fatal(err) - } - } -} - -func processSaas() { - if _, err := os.Stat(saasRoot); err != nil { - log.Fatal(err) - } - - removeDirs(saasRoot, "api", "gen", "pkg") - - processDirsFunc := makeProcessDirsFunc(saasRoot, saasFilePatch, []string{".go", ".proto"}, []string{"_test.go", "_fuzz.go"}) - - walk( - processDirsFunc, - "api/telemetry", - "gen/telemetry", "gen/utils/", - "pkg/logger", - ) - - // install and tidy to check if we have anything - _, err := runInDir(saasRoot, "go", "mod", "tidy") - if err != nil { - log.Fatal(err) - } - _, err = runInDir(saasRoot, "go", "install", "-v", "./...") - if err != nil { - log.Fatal(err) - } - - // check dependencies - b, err := runInDir(saasRoot, "go", "list", "-json", "./...") - if err != nil { - log.Fatal(err) - } - type packageInfo struct { - Dir string - Deps []string - } - d := json.NewDecoder(bytes.NewReader(b)) - for { - var info packageInfo - err = d.Decode(&info) - if err == io.EOF { - return - } - if err != nil { - log.Fatal(err) - } - for _, dep := range info.Deps { - if strings.Contains(dep, platformRepo) { - log.Fatalf("%s depends on platform module:\n%s", info.Dir, strings.Join(info.Deps, "\n")) - } - } - } -} - -func main() { - const saasProject = "saas" - - availableProjects := []string{ - saasProject, - } - - availableProjectsStr := strings.Join(availableProjects, " | ") - - project := flag.String("project", "", fmt.Sprintf("project to run post-processing for (%s)", availableProjectsStr)) - - flag.Parse() - - if flag.NFlag() > 1 { - flag.PrintDefaults() - log.Fatal("Too many arguments, use only one") - } - - if flag.NFlag() == 0 { - flag.PrintDefaults() - log.Fatal("You have to provide one argument") - } - - switch *project { - case saasProject: - processSaas() - default: - flag.PrintDefaults() - log.Fatal("Provide the target project name") - } -}