Skip to content

Commit 5ac5000

Browse files
authored
feat: permissions (#60)
* feat: parser permissions * chore: add tests for permissions * feat: add permissions class * update permissions once the dashboard has been created * feat: add permissions schema and docs * chore: update render.py to support permissions * chore: add tests for permissions
1 parent 5c44049 commit 5ac5000

File tree

16 files changed

+503
-33
lines changed

16 files changed

+503
-33
lines changed

README.md

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,11 @@
88
- Template panels and use them in multiple dashboards
99
- Mass update many dashboards quickly and easily
1010

11-
1211
## Install and quick start
1312

1413
Install `grafyaml`:
1514

16-
```
15+
```console
1716
pip3 install https://github.com/deliveryhero/grafyaml/archive/master.zip
1817
```
1918

@@ -33,11 +32,41 @@ dashboard:
3332
3433
Sync it to Grafana:
3534
36-
```
35+
```console
3736
export GRAFANA_API_KEY="API_KEY_HERE"
3837
grafyaml --grafana-url https://my-grafana-host.domain.com update my-example-dashboard.yaml
3938
```
4039

40+
## Permissions
41+
42+
Grafyaml supports permissions for dashboards. The permissions are defined in the same file as dashboard under `permissions` root key. The permissions are applied to the dashboard as a whole, not to individual panels.
43+
44+
```yaml
45+
46+
permissions:
47+
strategy: replace # merge or overwrite
48+
grants:
49+
# subject:identifier:permission
50+
- team:admins:admin
51+
dashboard:
52+
title: My Dashboard
53+
panels:
54+
- title: Container metrics
55+
panels:
56+
- title: Container CPU usage
57+
targets:
58+
- expr: rate(container_cpu_user_seconds_total[30s]) * 100
59+
type: timeseries
60+
```
61+
62+
Grants are structured as a triplet: `subject:identifier:permission`.
63+
64+
- Subject: Specifies the entity to which the permission applies. Valid subjects include team, user, role, or serviceAccount. (serviceAccount is not yet supported)
65+
- Identifier: The specific name or unique identifier of the subject.
66+
- Permission: Defines the level of access granted. Permissible values are view, edit, admin, or none.
67+
68+
Example: To grant edit permission for the developers team on a resource named my-dashboard, the grant would be defined as team:developers:edit.
69+
4170
## More examples
4271

4372
- [examples/basic](examples/basic): A very basic example with a single dashboard

examples/advanced/apps.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
app1:
2+
permissions:
3+
strategy: merge
4+
grants:
5+
- team:admins:admin
26
templating:
37
- name: dynamodb_name
48
query: dynamodb_name

examples/advanced/render.py

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ def write_dashboard_file(
274274
try:
275275
with open(output_file_path, "w") as f:
276276
# Use default_flow_style=False for a more human-readable output
277-
yaml.dump({"dashboard": dashboard_data}, f, default_flow_style=False)
277+
yaml.dump(dashboard_data, f, default_flow_style=False)
278278
logging.info(f"Successfully generated '{output_file_path}'")
279279
except (IOError, yaml.YAMLError) as e:
280280
logging.error(f"Error writing output file '{output_file_path}': {e}")
@@ -364,21 +364,25 @@ def main():
364364

365365
# --- Override Tags if specified ---
366366
app_tags = app_config.get("tags")
367-
if app_tags is not None: # Allows setting tags to None/empty list in input
368-
if (
369-
isinstance(app_tags, list) or app_tags is None
370-
): # Ensure it's a list or None
371-
dashboard_data["tags"] = app_tags
372-
logging.debug(f"Overrode tags for '{app_name}': {app_tags}")
373-
else:
374-
logging.warning(
375-
f"App '{app_name}' has invalid 'tags' structure: {app_tags}. "
376-
f"Expected list or null. Skipping tag override."
377-
)
367+
if app_tags and isinstance(app_tags, list):
368+
dashboard_data["tags"] = app_tags
369+
logging.debug(f"Overrode tags for '{app_name}': {app_tags}")
370+
else:
371+
logging.warning(
372+
f"App '{app_name}' has invalid 'tags' structure: {app_tags}. "
373+
f"Expected list or null. Skipping tag override."
374+
)
375+
376+
if "permissions" in app_config:
377+
dashboard_full_structure["permissions"] = app_config.get("permissions", [])
378378

379379
# --- Write Output File ---
380380
# Pass the dictionary that represents the content *under* the top-level 'dashboard' key
381-
write_dashboard_file(app_name, output_dir, dashboard_data)
381+
write_dashboard_file(
382+
app_name=app_name,
383+
output_dir=output_dir,
384+
dashboard_data=dashboard_full_structure,
385+
)
382386

383387
logging.info("Dashboard generation process completed.")
384388

examples/advanced/rendered_dashboards/.gitkeep

Whitespace-only changes.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
permissions:
2+
strategy: replace # merge or overwrite
3+
grants:
4+
# subject:identifier:permission
5+
- team:admins:admin
6+
- user:admin@localhost:edit
7+
- role:viewer:view
8+
- serviceAccount:grafana:admin
9+
10+
dashboard:
11+
editable: false
12+
tags:
13+
- grafyaml
14+
- example
15+
- basic
16+
time:
17+
from: now-30m
18+
to: now
19+
timezone: browser
20+
title: Host metrics
21+
rows:
22+
- showTitle: true
23+
title: CPU and memory usage
24+
collapse: false
25+
height: 500px
26+
panels:
27+
- fill: 8
28+
legend:
29+
alignAsTable: true
30+
current: true
31+
show: true
32+
sort: current
33+
sortDesc: true
34+
values: true
35+
span: 4
36+
stack: true
37+
targets:
38+
- expr: 100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) * on (instance) group_left (nodename) node_uname_info
39+
legendFormat: '{{ nodename }}'
40+
title: CPU usage
41+
type: graph
42+
- fill: 8
43+
legend:
44+
alignAsTable: true
45+
current: true
46+
show: true
47+
sort: current
48+
sortDesc: true
49+
values: true
50+
span: 4
51+
stack: true
52+
targets:
53+
- expr: 100 * (avg by (instance) (1 - ((avg_over_time(node_memory_MemFree_bytes{}[5m]) + avg_over_time(node_memory_Cached_bytes{}[5m]) + avg_over_time(node_memory_Buffers_bytes{}[5m])) / avg_over_time(node_memory_MemTotal_bytes{}[5m])))) * on (instance) group_left (nodename) node_uname_info
54+
legendFormat: '{{ nodename }}'
55+
title: Memory usage
56+
type: graph

grafana_dashboards/builder.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,15 @@ def _update_dashboard(self, data):
9696
for name in data:
9797
data, md5 = self.parser.get_dashboard(name)
9898
if self.cache.has_changed(name, md5):
99-
self.grafana.dashboard.create(
99+
permissions = self.parser.get_permissions(name)
100+
uid = self.grafana.dashboard.create(
100101
data=data, overwrite=self.overwrite, folder_id=self.folder_id
101102
)
103+
if uid and permissions:
104+
LOG.debug("Updating permissions for dashboard UID %s", uid)
105+
self.grafana.permissions.update(
106+
dashboard_uid=uid, permissions=permissions
107+
)
102108
self.cache.set(name, md5)
103109
else:
104110
LOG.debug("'%s' has not changed" % name)

grafana_dashboards/cmd.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ def update(self):
147147
builder = Builder(self.config)
148148
try:
149149
builder.update(self.args.path)
150-
except ValueError as e:
150+
except Exception as e:
151151
LOG.info(f"Error: {e}")
152152
sys.exit(1)
153153

grafana_dashboards/grafana/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
from grafana_dashboards.grafana.dashboard import Dashboard
1818
from grafana_dashboards.grafana.datasource import Datasource
19+
from grafana_dashboards.grafana.permissions import Permissions
1920

2021

2122
class Grafana(object):
@@ -45,3 +46,4 @@ def __init__(self, url, key=None):
4546

4647
self.dashboard = Dashboard(self.server, session)
4748
self.datasource = Datasource(self.server, session)
49+
self.permissions = Permissions(self.server, session)

grafana_dashboards/grafana/dashboard.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def __init__(self, base_url: str, session):
2424
self.search_url = utils.urljoin(base_url, "api/search?type=dash-db")
2525
self.session = session
2626

27-
def create(self, data: Dict, overwrite: bool = False, folder_id: int = 0) -> None:
27+
def create(self, data: Dict, overwrite: bool = False, folder_id: int = 0) -> str:
2828
"""Create a new dashboard
2929
3030
:param data: Dashboard model
@@ -63,6 +63,8 @@ def create(self, data: Dict, overwrite: bool = False, folder_id: int = 0) -> Non
6363
res = self.session.post(self.db_url, data=json.dumps(dashboard))
6464
res.raise_for_status()
6565

66+
return res.json().get("uid")
67+
6668
def delete(self, name):
6769
"""Delete a dashboard
6870
@@ -128,6 +130,10 @@ def find_dashboards_by_title(self, title: str, folder_id: int = 0) -> List[Dict]
128130
"""
129131
dashboards = self.search_dashboards(title)
130132

133+
# If folder_id is 0, return all dashboards
134+
if folder_id == 0:
135+
return dashboards
136+
131137
dashboards = list(
132138
filter(
133139
lambda x: x.get("title") == title and x.get("folderId") == folder_id,

0 commit comments

Comments
 (0)