grafana/apps/provisioning/pkg/connection/github/validator.go
Alejandro bd7dd42744
Provisioning: Add githubEnterprise repository and connection type (#124162)
* Add Github Enterprise Resource Type

* yarn run typecheck:tsgo

* lint

* i18n: extract github-enterprise wizard strings

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Pr comment

* PR suggestions first pass

* undo

* fix diff

* diff reduction

* fix frontend

* add minimal frontend

* reduce diff

* update codegen

* fix test

* gocyclo

* fix test

* fix snapshots

* fix test

* generate apis

* PR comments

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 08:22:36 -04:00

89 lines
3.6 KiB
Go

package github
import (
"context"
"encoding/base64"
"fmt"
"strconv"
"github.com/golang-jwt/jwt/v4"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/validation/field"
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
)
// Validate validates the github connection configuration without requiring decrypted secrets.
// This performs structural validation only - it does not verify the connection works.
func Validate(_ context.Context, obj runtime.Object) field.ErrorList {
conn, ok := obj.(*provisioning.Connection)
if !ok {
return nil
}
// Do nothing if connection is not github.
if conn.Spec.Type != provisioning.GithubConnectionType {
return nil
}
var list field.ErrorList
if conn.Spec.GitHub == nil {
list = append(
list, field.Required(field.NewPath("spec", "github"), "github info must be specified for GitHub connection"),
)
// Doesn't make much sense to continue validating a connection with no information.
return list
}
list = append(list, ValidateGitHubAppCredentials(conn, "GitHub", conn.Spec.GitHub.AppID, conn.Spec.GitHub.InstallationID, field.NewPath("spec", "github"))...)
return list
}
// ValidateGitHubAppCredentials performs structural validation of the GitHub App credential
// fields shared by github and githubEnterprise connections. label is interpolated into
// error messages (e.g. "GitHub", "GitHub Enterprise") so the source of the violation is
// clear. basePath is the field path of the spec section holding the credentials
// (e.g. spec.github or spec.githubEnterprise).
func ValidateGitHubAppCredentials(conn *provisioning.Connection, label, appID, installationID string, basePath *field.Path) field.ErrorList {
var list field.ErrorList
// Check if required secure values are present (without decryption)
if conn.Secure.PrivateKey.IsZero() {
list = append(list, field.Required(field.NewPath("secure", "privateKey"), fmt.Sprintf("privateKey must be specified for %s connection", label)))
}
if !conn.Secure.ClientSecret.IsZero() {
list = append(list, field.Forbidden(field.NewPath("secure", "clientSecret"), fmt.Sprintf("clientSecret is forbidden in %s connection", label)))
}
// Validate private key content if new is provided
if !conn.Secure.PrivateKey.Create.IsZero() {
// Decode base64-encoded private key
privateKeyPEM, err := base64.StdEncoding.DecodeString(string(conn.Secure.PrivateKey.Create))
if err != nil {
list = append(list, field.Invalid(field.NewPath("secure", "privateKey"), "[REDACTED]", "privateKey must be base64 encoded"))
} else {
// Parse the private key
_, err := jwt.ParseRSAPrivateKeyFromPEM(privateKeyPEM)
if err != nil {
list = append(list, field.Invalid(field.NewPath("secure", "privateKey"), "[REDACTED]", "privateKey must be a valid RSA private key"))
}
}
}
// Validate the existence and correctness of GitHub configuration fields.
// Skip the numeric check on empty values to avoid emitting both Required and Invalid for the same field.
if appID == "" {
list = append(list, field.Required(basePath.Child("appID"), fmt.Sprintf("appID must be specified for %s connection", label)))
} else if _, err := strconv.Atoi(appID); err != nil {
list = append(list, field.Invalid(basePath.Child("appID"), appID, "appID must be a numeric value"))
}
if installationID == "" {
list = append(list, field.Required(basePath.Child("installationID"), fmt.Sprintf("installationID must be specified for %s connection", label)))
} else if _, err := strconv.Atoi(installationID); err != nil {
list = append(list, field.Invalid(basePath.Child("installationID"), installationID, "installationID must be a numeric value"))
}
return list
}