Skip to content

Commit 4b811b6

Browse files
authored
Add support for GitHub App authentication (#363)
1 parent 67554cd commit 4b811b6

File tree

12 files changed

+217
-68
lines changed

12 files changed

+217
-68
lines changed

cspell.config.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"dfutil",
2121
"dserrors",
2222
"errorsource",
23+
"ghinstallation",
2324
"githubclient",
2425
"githubv",
2526
"googlegithub",

docs/sources/setup/datasource.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,15 @@ weight: 103
1717

1818
# Configure the GitHub data source plugin for Grafana
1919

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

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

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

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

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

3131
(_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.

docs/sources/setup/token.md

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
2-
title: Create a GitHub personal access token
3-
menuTitle: Create a personal access token
4-
description: Create a GitHub personal access token
2+
title: Create a GitHub access token
3+
menuTitle: Create an access token
4+
description: Create a GitHub access token
55
keywords:
66
- data source
77
- github
@@ -15,37 +15,38 @@ labels:
1515
weight: 102
1616
---
1717

18-
# Create a GitHub personal access token
18+
# Create a GitHub access token
1919

20-
You will need a _personal access token_ to use the plugin. GitHub currently supports two types of personal access tokens:
21-
22-
1. fine-grained personal access tokens
23-
1. personal access tokens (classic)
24-
25-
Read more about [personal access tokens](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens).
26-
27-
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.
28-
29-
Options:
30-
31-
| Setting | Required | Description |
32-
| --------------------- | -------- | ----------------------------------------------------- |
33-
| Access token | true | This is required to allow plugin to connect to GitHub |
34-
| GitHub Enterprise URL | false | Only if you are using GitHub Enterprise account |
20+
You will need either a `GitHub App` or a `Personal Access Token` to use this plugin.
3521

3622
## Creating a personal access token (classic)
3723

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

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

46-
### Permissions
33+
## Using GitHub App Authentication
34+
35+
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.
36+
37+
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).
38+
1. After registering the App, generate a private key for authentication.
39+
1. Note down the App ID assigned to your GitHub App.
40+
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.
41+
1. Note the installation ID after completing the installation.
42+
1. In Grafana's data source settings, provide the **app id**, **installation id**, and **private key** in the appropriate fields.
43+
44+
> **Where to find your installation id?** \
45+
> 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>`.
46+
47+
## Permissions
4748

48-
You will need to define the access permissions for your token in order to allow it to access the data.
49+
You will need to define the access permissions for your **personal access token** in order to allow it to access the data.
4950

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

go.mod

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
module github.com/grafana/github-datasource
22

3-
go 1.22
3+
go 1.23
44

5-
toolchain go1.22.1
5+
toolchain go1.23.1
66

77
require (
8+
github.com/bradleyfalzon/ghinstallation/v2 v2.11.0
89
github.com/google/go-github/v53 v53.2.0
910
github.com/grafana/grafana-plugin-sdk-go v0.250.0
1011
github.com/influxdata/tdigest v0.0.1
@@ -32,6 +33,8 @@ require (
3233
github.com/go-openapi/swag v0.22.8 // indirect
3334
github.com/goccy/go-json v0.10.2 // indirect
3435
github.com/gogo/protobuf v1.3.2 // indirect
36+
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
37+
github.com/google/go-github/v62 v62.0.0 // indirect
3538
github.com/google/go-querystring v1.1.0 // indirect
3639
github.com/google/uuid v1.6.0 // indirect
3740
github.com/gorilla/mux v1.8.1 // indirect

go.sum

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhP
99
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw=
1010
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
1111
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
12+
github.com/bradleyfalzon/ghinstallation/v2 v2.11.0 h1:R9d0v+iobRHSaE4wKUnXFiZp53AL4ED5MzgEMwGTZag=
13+
github.com/bradleyfalzon/ghinstallation/v2 v2.11.0/go.mod h1:0LWKQwOHewXO/1acI6TtyE0Xc4ObDb2rFN7eHBAG71M=
1214
github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA=
1315
github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8=
1416
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
@@ -60,6 +62,8 @@ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
6062
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
6163
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
6264
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
65+
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
66+
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
6367
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
6468
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
6569
github.com/google/flatbuffers v23.5.26+incompatible h1:M9dgRyhJemaM4Sw8+66GHBu8ioaQmyPLg1b8VwK5WJg=
@@ -70,6 +74,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
7074
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
7175
github.com/google/go-github/v53 v53.2.0 h1:wvz3FyF53v4BK+AsnvCmeNhf8AkTaeh2SoYu/XUvTtI=
7276
github.com/google/go-github/v53 v53.2.0/go.mod h1:XhFRObz+m/l+UCm9b7KSIC3lT3NWSXGt7mOsAWEloao=
77+
github.com/google/go-github/v62 v62.0.0 h1:/6mGCaRywZz9MuHyw9gD1CwsbmBX8GWsbFkwMmHdhl4=
78+
github.com/google/go-github/v62 v62.0.0/go.mod h1:EMxeUqGJq2xRu9DYBMwel/mr7kZrzUOfQmmpYrZn2a4=
7379
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
7480
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
7581
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=

pkg/github/client/client.go

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"strconv"
1010
"time"
1111

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

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

60+
if settings.SelectedAuthType == "personal-access-token" {
61+
return createAccessTokenClient(ctx, settings)
62+
}
63+
64+
return nil, errorsource.DownstreamError(fmt.Errorf("access token or app token are required"), false)
65+
}
66+
67+
func createAppClient(settings models.Settings) (*Client, error) {
68+
appId, err := strconv.ParseInt(settings.AppId, 10, 64)
69+
if err != nil {
70+
return nil, errorsource.DownstreamError(fmt.Errorf("error parsing app id"), false)
71+
}
72+
73+
installationId, err := strconv.ParseInt(settings.InstallationId, 10, 64)
74+
if err != nil {
75+
return nil, errorsource.DownstreamError(fmt.Errorf("error parsing installation id"), false)
76+
}
77+
78+
itr, err := ghinstallation.New(http.DefaultTransport, appId, installationId, []byte(settings.PrivateKey))
79+
if err != nil {
80+
return nil, errorsource.DownstreamError(fmt.Errorf("error creating token source"), false)
81+
}
82+
83+
httpClient := &http.Client{Transport: itr}
84+
85+
if settings.GitHubURL == "" {
86+
return &Client{
87+
restClient: googlegithub.NewClient(httpClient),
88+
graphqlClient: githubv4.NewClient(httpClient),
89+
}, nil
90+
}
91+
92+
return useGitHubEnterprise(httpClient, settings)
93+
}
94+
95+
func createAccessTokenClient(ctx context.Context, settings models.Settings) (*Client, error) {
6096
src := oauth2.StaticTokenSource(
6197
&oauth2.Token{AccessToken: settings.AccessToken},
6298
)
@@ -70,6 +106,10 @@ func New(ctx context.Context, settings models.Settings) (*Client, error) {
70106
}, nil
71107
}
72108

109+
return useGitHubEnterprise(httpClient, settings)
110+
}
111+
112+
func useGitHubEnterprise(httpClient *http.Client, settings models.Settings) (*Client, error) {
73113
_, err := url.Parse(settings.GitHubURL)
74114
if err != nil {
75115
return nil, errorsource.DownstreamError(fmt.Errorf("incorrect enterprise url"), false)

pkg/github/datasource.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package github
22

33
import (
44
"context"
5-
"fmt"
65
"strings"
76

87
"github.com/grafana/github-datasource/pkg/dfutil"
@@ -209,7 +208,7 @@ func (d *Datasource) QueryData(ctx context.Context, req *backend.QueryDataReques
209208
func NewDatasource(ctx context.Context, settings models.Settings) (*Datasource, error) {
210209
client, err := githubclient.New(ctx, settings)
211210
if err != nil {
212-
return nil, fmt.Errorf("instantiating github client: %w", err)
211+
return nil, err
213212
}
214213
return &Datasource{client: client}, nil
215214
}

pkg/models/settings.go

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,16 @@ import (
66
"github.com/grafana/grafana-plugin-sdk-go/backend"
77
)
88

9-
// Settings represents the Datasource options in Grafana
109
type Settings struct {
11-
AccessToken string `json:"accessToken"`
12-
GitHubURL string `json:"githubUrl"`
13-
CachingEnabled bool `json:"cachingEnabled"`
10+
SelectedAuthType string `json:"selectedAuthType"`
11+
AccessToken string `json:"accessToken"`
12+
PrivateKey string `json:"privateKey"`
13+
AppId string `json:"appId"`
14+
InstallationId string `json:"installationId"`
15+
GitHubURL string `json:"githubUrl"`
16+
CachingEnabled bool `json:"cachingEnabled"`
1417
}
1518

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

29+
if val, ok := settings.DecryptedSecureJSONData["privateKey"]; ok {
30+
s.PrivateKey = val
31+
}
32+
33+
// Data sources created before the auth type was introduced will have an accessToken but no auth type.
34+
// In this case, we default to personal access token.
35+
if s.AccessToken != "" && s.SelectedAuthType == "" {
36+
s.SelectedAuthType = "personal-access-token"
37+
}
38+
2739
return s, nil
2840
}

pkg/plugin/instance.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414
func NewGitHubInstance(ctx context.Context, settings models.Settings) (instancemgmt.Instance, error) {
1515
gh, err := github.NewDatasource(ctx, settings)
1616
if err != nil {
17-
return nil, fmt.Errorf("instantiating github datasource: %w", err)
17+
return nil, err
1818
}
1919

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

3838
instance, err := NewGitHubInstance(context.Background(), datasourceSettings)
3939
if err != nil {
40-
return instance, fmt.Errorf("instantiating github instance")
40+
return instance, fmt.Errorf("instantiating github instance: %w", err)
4141
}
4242

4343
return instance, nil

src/types.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,34 @@ export interface GitHubEnterpriseOptions {
1616
githubUrl?: string;
1717
}
1818

19-
export interface GitHubDataSourceOptions extends DataSourceJsonData, RepositoryOptions, GitHubEnterpriseOptions {
20-
// Any global settings
19+
export interface GitHubAppAuth {
20+
appId?: string;
21+
installationId?: string;
22+
}
23+
24+
export interface GitHubDataSourceOptions
25+
extends DataSourceJsonData,
26+
RepositoryOptions,
27+
GitHubEnterpriseOptions,
28+
GitHubAppAuth {
29+
selectedAuthType?: GitHubAuthType;
2130
}
2231

2332
export interface GitHubSecureJsonData {
2433
// accessToken is set if the user is using a Personal Access Token to connect to GitHub
2534
accessToken?: string;
35+
// privateKey is set if the user is using a GitHub App to connect to GitHub
36+
privateKey?: string;
37+
}
38+
39+
export enum GitHubAuthType {
40+
Personal = 'personal-access-token',
41+
App = 'github-app',
42+
}
43+
44+
export enum GitHubLicenseType {
45+
Basic = 'github-basic',
46+
Enterprise = 'github-enterprise',
2647
}
2748

2849
export enum QueryType {

0 commit comments

Comments
 (0)