Skip to content

Commit fb67613

Browse files
committed
Add HeaderAuth authentication mechanism
It uses an arbitrary HTTP header for authentication, where the user can specify both its name and value. Synopsis:: from grafana_client import GrafanaApi, HeaderAuth grafana = GrafanaApi.from_url( url="https://daq.example.org/grafana/", credential=HeaderAuth(name="X-WEBAUTH-USER", value="foobar"), )
1 parent 0ee7810 commit fb67613

File tree

7 files changed

+63
-17
lines changed

7 files changed

+63
-17
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
* Update the `update_folder` method of the folder API to allow changing
66
the UID of the folder. Thanks, @iNoahNothing.
77
* Add `update_datasource_by_uid` to the datasource API. Thanks, @mgreen-sm.
8+
* Add `HeaderAuth` authentication mechanism, using an arbitrary HTTP header for
9+
authentication, where the user can specify both its name and value. Thanks, @l0tzi.
810

911

1012
## 3.0.0 (2022-07-02)

README.md

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -84,22 +84,31 @@ exercises could be useful for others, don't hesitate to share them back.
8484

8585
## Authentication
8686

87-
There are two ways to authenticate to the Grafana API. Either use an API token,
88-
or HTTP basic auth.
87+
There are several ways to authenticate to the Grafana HTTP API.
8988

90-
To use the admin API, you need to use HTTP Basic Authentication, as stated at
91-
the [Grafana Admin API documentation].
89+
1. Anonymous access
90+
2. Grafana API token
91+
3. HTTP Basic Authentication
92+
4. HTTP Header Authentication
93+
94+
The [Grafana Admin API] is a subset of the Grafana API. For accessing those API
95+
resources, you will need to use HTTP Basic Authentication.
9296

9397
```python
94-
from grafana_client import GrafanaApi
98+
from grafana_client import GrafanaApi, HeaderAuth, TokenAuth
99+
100+
# 1. Anonymous access
101+
grafana = GrafanaApi.from_url(
102+
url="https://daq.example.org/grafana/",
103+
)
95104

96-
# Use Grafana API token.
105+
# 2. Use Grafana API token.
97106
grafana = GrafanaApi.from_url(
98107
url="https://daq.example.org/grafana/",
99-
credential="eyJrIjoiWHg...dGJpZCI6MX0=",
108+
credential=TokenAuth(token="eyJrIjoiWHg...dGJpZCI6MX0="),
100109
)
101110

102-
# Use HTTP basic authentication.
111+
# 3. Use HTTP basic authentication.
103112
grafana = GrafanaApi.from_url(
104113
url="https://username:[email protected]/grafana/",
105114
)
@@ -108,6 +117,12 @@ grafana = GrafanaApi.from_url(
108117
credential=("username", "password")
109118
)
110119

120+
# 4. Use HTTP Header authentication.
121+
grafana = GrafanaApi.from_url(
122+
url="https://daq.example.org/grafana/",
123+
credential=HeaderAuth(name="X-WEBAUTH-USER", value="foobar"),
124+
)
125+
111126
# Optionally turn off TLS certificate verification.
112127
grafana = GrafanaApi.from_url(
113128
url="https://username:[email protected]/grafana/?verify=false",
@@ -117,6 +132,10 @@ grafana = GrafanaApi.from_url(
117132
grafana = GrafanaApi.from_env()
118133
```
119134

135+
Please note that, on top of the specific examples above, the object obtained by
136+
`credential` can be an arbitrary `requests.auth.AuthBase` instance.
137+
138+
120139
## Proxy
121140

122141
The underlying `requests` library honors the `HTTP_PROXY` and `HTTPS_PROXY`
@@ -265,6 +284,6 @@ poe test
265284
[examples folder]: https://github.com/panodata/grafana-client/tree/main/examples
266285
[future maintenance of `grafana_api`]: https://github.com/m0nhawk/grafana_api/issues/88
267286
[grafana_api]: https://github.com/m0nhawk/grafana_api
268-
[Grafana Admin API documentation]: https://grafana.com/docs/grafana/latest/http_api/admin/
287+
[Grafana Admin API]: https://grafana.com/docs/grafana/latest/http_api/admin/
269288
[Grafana HTTP API reference]: https://grafana.com/docs/grafana/latest/http_api/
270289
[LICENSE]: https://github.com/panodata/grafana-client/blob/main/LICENSE

grafana_client/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
warnings.filterwarnings("ignore", category=Warning, message="distutils Version classes are deprecated")
99
from .api import GrafanaApi # noqa:F401
10+
from .client import HeaderAuth, TokenAuth # noqa:F401
1011

1112
__appname__ = "grafana-client"
1213

grafana_client/api.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from urllib.parse import parse_qs, urlparse
77

88
import requests
9+
import requests.auth
910
from urllib3.exceptions import InsecureRequestWarning
1011

1112
from .client import GrafanaClient
@@ -88,7 +89,7 @@ def version(self):
8889
return version
8990

9091
@classmethod
91-
def from_url(cls, url: str = None, credential: Union[str, Tuple[str, str]] = None):
92+
def from_url(cls, url: str = None, credential: Union[str, Tuple[str, str], requests.auth.AuthBase] = None):
9293
"""
9394
Factory method to create a `GrafanaApi` instance from a URL.
9495
@@ -100,7 +101,7 @@ def from_url(cls, url: str = None, credential: Union[str, Tuple[str, str]] = Non
100101
if url is None:
101102
url = "http://admin:admin@localhost:3000"
102103

103-
if credential is not None and not isinstance(credential, (str, Tuple)):
104+
if credential is not None and not isinstance(credential, (str, Tuple, requests.auth.AuthBase)):
104105
raise TypeError(f"Argument 'credential' has wrong type: {type(credential)}")
105106

106107
original_url = url

grafana_client/client.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,16 @@ def __call__(self, request):
5454
return request
5555

5656

57+
class HeaderAuth(requests.auth.AuthBase):
58+
def __init__(self, name, value):
59+
self.name = name
60+
self.value = value
61+
62+
def __call__(self, request):
63+
request.headers.update({self.name: self.value})
64+
return request
65+
66+
5767
class GrafanaClient:
5868
def __init__(
5969
self,
@@ -98,10 +108,12 @@ def construct_api_url():
98108
self.s = requests.Session()
99109
self.s.headers["User-Agent"] = self.user_agent
100110
if self.auth is not None:
101-
if not isinstance(self.auth, tuple):
102-
self.auth = TokenAuth(self.auth)
103-
else:
111+
if isinstance(self.auth, requests.auth.AuthBase):
112+
pass
113+
elif isinstance(self.auth, tuple):
104114
self.auth = requests.auth.HTTPBasicAuth(*self.auth)
115+
else:
116+
self.auth = TokenAuth(self.auth)
105117

106118
def __getattr__(self, item):
107119
def __request_runner(url, json=None, data=None, headers=None):

test/test_grafana_api.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44

55
import requests
66

7-
from grafana_client import GrafanaApi
8-
from grafana_client.client import TokenAuth
7+
from grafana_client import GrafanaApi, HeaderAuth, TokenAuth
98

109

1110
class TestGrafanaApiFactories(unittest.TestCase):
@@ -41,6 +40,12 @@ def test_from_url_tokenauth(self):
4140
self.assertIsInstance(grafana.client.auth, TokenAuth)
4241
self.assertEqual(grafana.client.auth.token, "VerySecretToken")
4342

43+
def test_from_url_headerauth(self):
44+
grafana = GrafanaApi.from_url(credential=HeaderAuth(name="X-WEBAUTH-USER", value="foobar"))
45+
self.assertIsInstance(grafana.client.auth, HeaderAuth)
46+
self.assertEqual(grafana.client.auth.name, "X-WEBAUTH-USER")
47+
self.assertEqual(grafana.client.auth.value, "foobar")
48+
4449
def test_from_url_auth_precedence(self):
4550
grafana = GrafanaApi.from_url("http://foo:bar@localhost:3000", credential="VerySecretToken")
4651
self.assertIsInstance(grafana.client.auth, TokenAuth)

test/test_grafana_client.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import requests
1010

1111
from grafana_client.api import GrafanaApi
12-
from grafana_client.client import GrafanaClientError, TokenAuth
12+
from grafana_client.client import GrafanaClientError, HeaderAuth, TokenAuth
1313

1414

1515
class MockResponse:
@@ -118,6 +118,12 @@ def test_tokenauth(self):
118118
tokenauth(request)
119119
self.assertEqual(request.headers["Authorization"], "Bearer VerySecretToken")
120120

121+
def test_headerauth(self):
122+
headerauth = HeaderAuth(name="X-WEBAUTH-USER", value="foobar")
123+
request = requests.Request()
124+
headerauth(request)
125+
self.assertEqual(request.headers["X-WEBAUTH-USER"], "foobar")
126+
121127
@patch("grafana_client.client.GrafanaClient.__getattr__")
122128
def test_grafana_client_connect_success(self, mock_get):
123129
payload = {"commit": "14e988bd22", "database": "ok", "version": "9.0.1"}

0 commit comments

Comments
 (0)