Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for GitHub App authentication #363

Merged
merged 18 commits into from
Sep 27, 2024
Merged
1 change: 1 addition & 0 deletions cspell.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"dfutil",
"dserrors",
"errorsource",
"ghinstallation",
"githubclient",
"githubv",
"googlegithub",
Expand Down
4 changes: 2 additions & 2 deletions docs/sources/setup/datasource.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ weight: 103

# Configure the GitHub data source plugin for Grafana

1. After creating the **personal access token** in GitHub, navigate into Grafana and click on the menu option on the top left.
1. After creating the **access token** in GitHub, navigate into Grafana and click on the menu option on the top left.

1. Browse to the **Connections** menu and then click on the **Data sources**.

1. Click on the GitHub data source plugin which you have installed.

1. Go to its settings tab and at the bottom, you will find the **Connection** section.

1. Paste the personal access token.
1. Paste the access token.
![Configuring API Token](/media/docs/grafana/data-sources/github/github-plugin-confg-token.png)

(_Optional_): If you using the GitHub Enterprise, then select the **Enterprise** option inside the **Additional Settings** section and paste the URL of your GitHub Enterprise.
Expand Down
45 changes: 23 additions & 22 deletions docs/sources/setup/token.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: Create a GitHub personal access token
menuTitle: Create a personal access token
description: Create a GitHub personal access token
title: Create a GitHub access token
menuTitle: Create an access token
description: Create a GitHub access token
keywords:
- data source
- github
Expand All @@ -15,37 +15,38 @@ labels:
weight: 102
---

# Create a GitHub personal access token
# Create a GitHub access token

You will need a _personal access token_ to use the plugin. GitHub currently supports two types of personal access tokens:

1. fine-grained personal access tokens
1. personal access tokens (classic)

Read more about [personal access tokens](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens).

The Grafana GitHub data source plugin works with both. Below is a table that indicates what minimum requirements must be matched before the plugin can be used.

Options:

| Setting | Required | Description |
| --------------------- | -------- | ----------------------------------------------------- |
| Access token | true | This is required to allow plugin to connect to GitHub |
| GitHub Enterprise URL | false | Only if you are using GitHub Enterprise account |
You will need either a `GitHub App` or a `Personal Access Token` to use this plugin.

## Creating a personal access token (classic)

This is an example when you want to use the personal access token (classic).
This is an example when you want to use the personal access token (classic). \
Read more about [personal access tokens](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens).

1. Login to your GitHub account.
1. Navigate to [Personal access tokens](https://github.com/settings/tokens) and click **Generate new token**.
1. Select the **personal access token (classic)**.
1. Define the permissions which you want to allow.
1. Click **Generate Token**.

### Permissions
## Using GitHub App Authentication

You can also authenticate using a GitHub App instead of a personal access token. This method allows for better security and fine-grained access to resources.

1. Register a new GitHub App by following the instructions in the [GitHub App documentation](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app).
1. After registering the App, generate a private key for authentication.
1. Note down the App ID assigned to your GitHub App.
1. [Install the GitHub App](https://docs.github.com/en/apps/using-github-apps/installing-your-own-github-app) on your GitHub account or organization.
1. Note the installation ID after completing the installation.
1. In Grafana's data source settings, provide the **app id**, **installation id**, and **private key** in the appropriate fields.

> **Where to find your installation id?** \
> Head over to Settings > Installed GitHub Apps > Configure. The installation ID can be found at the end of the URL `https://github.com/settings/installations/<installation_id>`.

## Permissions

You will need to define the access permissions for your token in order to allow it to access the data.
You will need to define the access permissions for your **personal access token** in order to allow it to access the data.

The following lists include the required permissions for the access token:

Expand Down
7 changes: 5 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
module github.com/grafana/github-datasource

go 1.22
go 1.23

toolchain go1.22.1
toolchain go1.23.1

require (
github.com/bradleyfalzon/ghinstallation/v2 v2.11.0
github.com/google/go-github/v53 v53.2.0
github.com/grafana/grafana-plugin-sdk-go v0.250.0
github.com/influxdata/tdigest v0.0.1
Expand Down Expand Up @@ -32,6 +33,8 @@ require (
github.com/go-openapi/swag v0.22.8 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/google/go-github/v62 v62.0.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/mux v1.8.1 // indirect
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhP
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bradleyfalzon/ghinstallation/v2 v2.11.0 h1:R9d0v+iobRHSaE4wKUnXFiZp53AL4ED5MzgEMwGTZag=
github.com/bradleyfalzon/ghinstallation/v2 v2.11.0/go.mod h1:0LWKQwOHewXO/1acI6TtyE0Xc4ObDb2rFN7eHBAG71M=
github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA=
github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8=
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
Expand Down Expand Up @@ -60,6 +62,8 @@ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/flatbuffers v23.5.26+incompatible h1:M9dgRyhJemaM4Sw8+66GHBu8ioaQmyPLg1b8VwK5WJg=
Expand All @@ -70,6 +74,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-github/v53 v53.2.0 h1:wvz3FyF53v4BK+AsnvCmeNhf8AkTaeh2SoYu/XUvTtI=
github.com/google/go-github/v53 v53.2.0/go.mod h1:XhFRObz+m/l+UCm9b7KSIC3lT3NWSXGt7mOsAWEloao=
github.com/google/go-github/v62 v62.0.0 h1:/6mGCaRywZz9MuHyw9gD1CwsbmBX8GWsbFkwMmHdhl4=
github.com/google/go-github/v62 v62.0.0/go.mod h1:EMxeUqGJq2xRu9DYBMwel/mr7kZrzUOfQmmpYrZn2a4=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
Expand Down
46 changes: 43 additions & 3 deletions pkg/github/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"strconv"
"time"

"github.com/bradleyfalzon/ghinstallation/v2"
googlegithub "github.com/google/go-github/v53/github"
"github.com/grafana/github-datasource/pkg/models"
"github.com/grafana/grafana-plugin-sdk-go/backend"
Expand Down Expand Up @@ -52,11 +53,46 @@ var runnerPerMinuteRate = map[string]float64{

// New instantiates a new GitHub API client.
func New(ctx context.Context, settings models.Settings) (*Client, error) {
if settings.AccessToken == "" {
// If access token is not set, return downstream error as it is required.
return nil, errorsource.DownstreamError(fmt.Errorf("access token is required"), false)
if settings.SelectedAuthType == "github-app" {
return createAppClient(settings)
}

if settings.SelectedAuthType == "personal-access-token" {
gwdawson marked this conversation as resolved.
Show resolved Hide resolved
return createAccessTokenClient(ctx, settings)
}

return nil, errorsource.DownstreamError(fmt.Errorf("access token or app token are required"), false)
}

func createAppClient(settings models.Settings) (*Client, error) {
appId, err := strconv.ParseInt(settings.AppId, 10, 64)
if err != nil {
return nil, errorsource.DownstreamError(fmt.Errorf("error parsing app id"), false)
}

installationId, err := strconv.ParseInt(settings.InstallationId, 10, 64)
if err != nil {
return nil, errorsource.DownstreamError(fmt.Errorf("error parsing installation id"), false)
}

itr, err := ghinstallation.New(http.DefaultTransport, appId, installationId, []byte(settings.PrivateKey))
if err != nil {
return nil, errorsource.DownstreamError(fmt.Errorf("error creating token source"), false)
}

httpClient := &http.Client{Transport: itr}

if settings.GitHubURL == "" {
return &Client{
restClient: googlegithub.NewClient(httpClient),
graphqlClient: githubv4.NewClient(httpClient),
}, nil
}

return useGitHubEnterprise(httpClient, settings)
}

func createAccessTokenClient(ctx context.Context, settings models.Settings) (*Client, error) {
src := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: settings.AccessToken},
)
Expand All @@ -70,6 +106,10 @@ func New(ctx context.Context, settings models.Settings) (*Client, error) {
}, nil
}

return useGitHubEnterprise(httpClient, settings)
}

func useGitHubEnterprise(httpClient *http.Client, settings models.Settings) (*Client, error) {
_, err := url.Parse(settings.GitHubURL)
if err != nil {
return nil, errorsource.DownstreamError(fmt.Errorf("incorrect enterprise url"), false)
Expand Down
3 changes: 1 addition & 2 deletions pkg/github/datasource.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package github

import (
"context"
"fmt"
"strings"

"github.com/grafana/github-datasource/pkg/dfutil"
Expand Down Expand Up @@ -209,7 +208,7 @@ func (d *Datasource) QueryData(ctx context.Context, req *backend.QueryDataReques
func NewDatasource(ctx context.Context, settings models.Settings) (*Datasource, error) {
client, err := githubclient.New(ctx, settings)
if err != nil {
return nil, fmt.Errorf("instantiating github client: %w", err)
return nil, err
}
return &Datasource{client: client}, nil
}
Expand Down
22 changes: 17 additions & 5 deletions pkg/models/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/backend"
)

// Settings represents the Datasource options in Grafana
type Settings struct {
AccessToken string `json:"accessToken"`
GitHubURL string `json:"githubUrl"`
CachingEnabled bool `json:"cachingEnabled"`
SelectedAuthType string `json:"selectedAuthType"`
AccessToken string `json:"accessToken"`
PrivateKey string `json:"privateKey"`
AppId string `json:"appId"`
InstallationId string `json:"installationId"`
GitHubURL string `json:"githubUrl"`
CachingEnabled bool `json:"cachingEnabled"`
}

// LoadSettings converts the DataSourceInLoadSettings to usable GitHub settings
func LoadSettings(settings backend.DataSourceInstanceSettings) (Settings, error) {
s := Settings{}
if err := json.Unmarshal(settings.JSONData, &s); err != nil {
Expand All @@ -24,5 +26,15 @@ func LoadSettings(settings backend.DataSourceInstanceSettings) (Settings, error)
s.AccessToken = val
}

if val, ok := settings.DecryptedSecureJSONData["privateKey"]; ok {
s.PrivateKey = val
}

// Data sources created before the auth type was introduced will have an accessToken but no auth type.
// In this case, we default to personal access token.
if s.AccessToken != "" && s.SelectedAuthType == "" {
s.SelectedAuthType = "personal-access-token"
}

return s, nil
}
4 changes: 2 additions & 2 deletions pkg/plugin/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
func NewGitHubInstance(ctx context.Context, settings models.Settings) (instancemgmt.Instance, error) {
gh, err := github.NewDatasource(ctx, settings)
if err != nil {
return nil, fmt.Errorf("instantiating github datasource: %w", err)
return nil, err
}

var d Datasource = gh
Expand All @@ -37,7 +37,7 @@ func NewDataSourceInstance(_ context.Context, settings backend.DataSourceInstanc

instance, err := NewGitHubInstance(context.Background(), datasourceSettings)
if err != nil {
return instance, fmt.Errorf("instantiating github instance")
return instance, fmt.Errorf("instantiating github instance: %w", err)
}

return instance, nil
Expand Down
25 changes: 23 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,34 @@
githubUrl?: string;
}

export interface GitHubDataSourceOptions extends DataSourceJsonData, RepositoryOptions, GitHubEnterpriseOptions {
// Any global settings
export interface GitHubAppAuth {
appId?: string;
installationId?: string;
}

export interface GitHubDataSourceOptions
extends DataSourceJsonData,
RepositoryOptions,
GitHubEnterpriseOptions,
GitHubAppAuth {
selectedAuthType?: GitHubAuthType;
}

export interface GitHubSecureJsonData {
// accessToken is set if the user is using a Personal Access Token to connect to GitHub
accessToken?: string;
// privateKey is set if the user is using a GitHub App to connect to GitHub
privateKey?: string;
}

export enum GitHubAuthType {
Personal = 'personal-access-token',
App = 'github-app',
}

export enum GitHubLicenseType {
Basic = 'github-basic',
Enterprise = 'github-enterprise',
}

export enum QueryType {
Expand Down Expand Up @@ -133,7 +154,7 @@
USER = 1,
}

export interface GitHubQuery extends Indexable, DataQuery, RepositoryOptions {

Check warning on line 157 in src/types.ts

View workflow job for this annotation

GitHub Actions / Build, lint and unit tests

'DataQuery' is deprecated. use the type from
options?:
| PullRequestsOptions
| ReleasesOptions
Expand Down
2 changes: 2 additions & 0 deletions src/views/ConfigEditor.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ describe('Config Editor', () => {
const options = { jsonData: {}, secureJsonFields: {} } as any;
render(<ConfigEditor options={options} onOptionsChange={onOptionsChange} />);
await waitFor(() => expect(screen.getByText('Additional Settings')).toBeInTheDocument());
onOptionsChange.mockClear(); // this is called on component render, so we need to clear it to avoid false positives
expect(screen.getByLabelText('Basic')).toBeChecked();
expect(screen.getByLabelText('Enterprise')).not.toBeChecked();
expect(screen.queryByText('GitHub Enterprise URL')).not.toBeInTheDocument();
Expand All @@ -22,6 +23,7 @@ describe('Config Editor', () => {
const options = { jsonData: { githubUrl: 'https://foo.bar' }, secureJsonFields: {} } as any;
render(<ConfigEditor options={options} onOptionsChange={onOptionsChange} />);
await waitFor(() => expect(screen.getByText('Additional Settings')).toBeInTheDocument());
onOptionsChange.mockClear();
expect(screen.getByLabelText('Basic')).not.toBeChecked();
expect(screen.getByLabelText('Enterprise')).toBeChecked();
expect(screen.queryByText('GitHub Enterprise URL')).toBeInTheDocument();
Expand Down
Loading
Loading