/
githubmirror
/
grafana
Обзор
Документация
Войти
/
githubmirror
/
grafana
Код
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
apps/provisioning/pkg/connection/github/client.go
261 строка
8 KB
Alejandro
Provisioning: (3/8) Rework connection token validation (#129740)
31 июл 2026, 19:34
Не верифицирован
31 июл 2026, 19:34
3d59912
Код
Авторство
О чём код?
package github import ( "context" "errors" "fmt" "net/http" "strconv" "time" "github.com/google/go-github/v82/github" apierrors "k8s.io/apimachinery/pkg/api/errors" "github.com/grafana/grafana/apps/provisioning/pkg/connection" ) // API errors that we need to convey after parsing real GH errors (or faking them). var ( ErrServiceUnavailable = apierrors.NewServiceUnavailable("github is unavailable") ErrNotFound = errors.New("not found") ErrUnprocessableEntity = errors.New("unprocessable entity") ) //go:generate mockery --name Client --structname MockClient --inpackage --filename client_mock.go --with-expecter type Client interface { GetApp(ctx context.Context) (App, error) GetAppInstallation(ctx context.Context, installationID string) (AppInstallation, error) ListInstallationRepositories(ctx context.Context) ([]Repository, error) CreateInstallationAccessToken(ctx context.Context, installationID string, repo string) (InstallationToken, error) } // Repository represents a GitHub repository accessible through an installation. type Repository struct { // Name of the repository Name string // Owner is the user or organization that owns the repository Owner string // URL of the repository (HTML URL) URL string } // App represents a Github App. type App struct { // ID represents the GH app ID. ID int64 // Slug represents the GH app slug. Slug string // Owner represents the GH account/org owning the app Owner string // Permissions granted to the GitHub App Permissions Permissions } type Permission int const ( PermissionNone Permission = iota PermissionRead PermissionWrite ) // Permissions represents the permissions granted to a GitHub Apps and their installations. type Permissions struct { Contents Permission Metadata Permission PullRequests Permission Webhooks Permission } // AppInstallation represents a Github App Installation. type AppInstallation struct { // ID represents the GH installation ID. ID int64 // Whether the installation is enabled or not. Enabled bool // Permissions granted to this installation. // These may differ from App permissions if the installation owner has not yet accepted // the App's updated permissions on GitHub. Permissions Permissions } // InstallationToken represents a Github App Installation Access Token. type InstallationToken struct { // Token is the access token value. Token string // ExpiresAt is the expiration time of the token. ExpiresAt time.Time } type githubClient struct { gh *github.Client } func NewClient(client *github.Client) Client { return &githubClient{gh: client} } // GetApp gets the app by using the given token. func (r *githubClient) GetApp(ctx context.Context) (App, error) { app, _, err := r.gh.Apps.Get(ctx, "") if err != nil { var ghErr *github.ErrorResponse if errors.As(err, &ghErr) && ghErr.Response != nil { switch ghErr.Response.StatusCode { case http.StatusUnauthorized, http.StatusForbidden: return App{}, connection.ErrAuthentication case http.StatusNotFound: return App{}, fmt.Errorf("app: %w", ErrNotFound) case http.StatusServiceUnavailable: return App{}, ErrServiceUnavailable } } return App{}, err } return App{ ID: app.GetID(), Slug: app.GetSlug(), Owner: app.GetOwner().GetLogin(), Permissions: Permissions{ Contents: toPermission(app.GetPermissions().GetContents()), Metadata: toPermission(app.GetPermissions().GetMetadata()), PullRequests: toPermission(app.GetPermissions().GetPullRequests()), Webhooks: toPermission(app.GetPermissions().GetRepositoryHooks()), }, }, nil } // GetAppInstallation gets the installation of the app related to the given token. func (r *githubClient) GetAppInstallation(ctx context.Context, installationID string) (AppInstallation, error) { id, err := strconv.Atoi(installationID) if err != nil { return AppInstallation{}, fmt.Errorf("invalid installation ID: %s", installationID) } installation, _, err := r.gh.Apps.GetInstallation(ctx, int64(id)) if err != nil { var ghErr *github.ErrorResponse if errors.As(err, &ghErr) && ghErr.Response != nil { switch ghErr.Response.StatusCode { case http.StatusUnauthorized, http.StatusForbidden: return AppInstallation{}, connection.ErrAuthentication case http.StatusNotFound: return AppInstallation{}, fmt.Errorf("installation: %w", ErrNotFound) case http.StatusServiceUnavailable: return AppInstallation{}, ErrServiceUnavailable } } return AppInstallation{}, err } return AppInstallation{ ID: installation.GetID(), Enabled: installation.GetSuspendedAt().IsZero(), Permissions: Permissions{ Contents: toPermission(installation.GetPermissions().GetContents()), Metadata: toPermission(installation.GetPermissions().GetMetadata()), PullRequests: toPermission(installation.GetPermissions().GetPullRequests()), Webhooks: toPermission(installation.GetPermissions().GetRepositoryHooks()), }, }, nil } func toPermission(permissions string) Permission { switch permissions { case "read": return PermissionRead case "write": return PermissionWrite default: return PermissionNone } } const ( maxRepositories = 1000 // Maximum number of repositories to fetch ) // ListInstallationRepositories lists all repositories accessible by the specified GitHub App installation. func (r *githubClient) ListInstallationRepositories(ctx context.Context) ([]Repository, error) { var allRepos []Repository opts := &github.ListOptions{ Page: 1, PerPage: 100, } for { result, resp, err := r.gh.Apps.ListRepos(ctx, opts) if err != nil { var ghErr *github.ErrorResponse if errors.As(err, &ghErr) && ghErr.Response.StatusCode == http.StatusServiceUnavailable { return nil, ErrServiceUnavailable } return nil, fmt.Errorf("list repositories: %w", err) } for _, repo := range result.Repositories { allRepos = append(allRepos, Repository{ Name: repo.GetName(), Owner: repo.GetOwner().GetLogin(), URL: repo.GetHTMLURL(), }) } // Check if we've exceeded the maximum allowed repositories if len(allRepos) > maxRepositories { return nil, fmt.Errorf("too many repositories to fetch (more than %d)", maxRepositories) } // If there are no more pages, break if resp.NextPage == 0 { break } opts.Page = resp.NextPage } return allRepos, nil } // CreateInstallationAccessToken creates an installation access token scoped to a specific repository. // Based on https://docs.github.com/en/rest/apps/apps?apiVersion=latest#create-an-installation-access-token-for-an-app // the installation access token will last one hour from the time it is created. func (r *githubClient) CreateInstallationAccessToken(ctx context.Context, installationID string, repo string) (InstallationToken, error) { id, err := strconv.Atoi(installationID) if err != nil { return InstallationToken{}, fmt.Errorf("invalid installation ID: %s", installationID) } var opts *github.InstallationTokenOptions if repo != "" { opts = &github.InstallationTokenOptions{ Repositories: []string{repo}, } } token, _, err := r.gh.Apps.CreateInstallationToken(ctx, int64(id), opts) if err != nil { var ghErr *github.ErrorResponse if errors.As(err, &ghErr) { switch ghErr.Response.StatusCode { case http.StatusServiceUnavailable: return InstallationToken{}, ErrServiceUnavailable case http.StatusUnauthorized, http.StatusForbidden: return InstallationToken{}, connection.ErrAuthentication case http.StatusNotFound: // Not Found is returned by this API when the given installation is not present. return InstallationToken{}, fmt.Errorf("installation: %w", ErrNotFound) case http.StatusUnprocessableEntity: return InstallationToken{}, fmt.Errorf("%s: %w", ghErr.Message, ErrUnprocessableEntity) } } return InstallationToken{}, err } return InstallationToken{ Token: token.GetToken(), ExpiresAt: token.GetExpiresAt().Time, }, nil }