Skip to content

Commit 7d3e6f2

Browse files
authored
Merge pull request #4920 from grafana/dev
v1.9.13
2 parents 86940ef + a25d44d commit 7d3e6f2

File tree

8 files changed

+143
-16
lines changed

8 files changed

+143
-16
lines changed

engine/apps/grafana_plugin/helpers/client.py

+10
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,8 @@ class GrafanaAPIClient(APIClient):
166166

167167
USER_PERMISSION_ENDPOINT = f"api/access-control/users/permissions/search?actionPrefix={ACTION_PREFIX}"
168168

169+
MIN_GRAFANA_TOKEN_LENGTH = 16
170+
169171
class Types:
170172
class _BaseGrafanaAPIResponse(typing.TypedDict):
171173
totalCount: int
@@ -330,6 +332,14 @@ def get_service_account_token_permissions(self) -> APIClientResponse[typing.Dict
330332
def sync(self) -> APIClientResponse:
331333
return self.api_post("api/plugins/grafana-oncall-app/resources/plugin/sync")
332334

335+
@staticmethod
336+
def validate_grafana_token_format(grafana_token: str) -> bool:
337+
if not grafana_token or not isinstance(grafana_token, str):
338+
return False
339+
if len(grafana_token) < GrafanaAPIClient.MIN_GRAFANA_TOKEN_LENGTH:
340+
return False
341+
return True
342+
333343

334344
class GcomAPIClient(APIClient):
335345
ACTIVE_INSTANCE_QUERY = "instances?status=active"

engine/apps/grafana_plugin/helpers/gcom.py

+15-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from apps.auth_token.exceptions import InvalidToken
88
from apps.auth_token.models import PluginAuthToken
9-
from apps.grafana_plugin.helpers import GcomAPIClient
9+
from apps.grafana_plugin.helpers import GcomAPIClient, GrafanaAPIClient
1010
from apps.user_management.models import Organization
1111

1212
logger = logging.getLogger(__name__)
@@ -45,13 +45,20 @@ def check_gcom_permission(token_string: str, context) -> GcomToken:
4545
if not instance_info or str(instance_info["orgId"]) != org_id:
4646
raise InvalidToken
4747

48+
grafana_token_format_is_valid = GrafanaAPIClient.validate_grafana_token_format(grafana_token)
49+
4850
if not organization:
4951
from apps.base.models import DynamicSetting
5052

5153
allow_signup = DynamicSetting.objects.get_or_create(
5254
name="allow_plugin_organization_signup", defaults={"boolean_value": True}
5355
)[0].boolean_value
5456
if allow_signup:
57+
if not grafana_token_format_is_valid:
58+
logger.debug(
59+
f"grafana token sent when creating stack_id={stack_id} was invalid format. api_token will still be written to DB"
60+
)
61+
5562
# Get org from db or create a new one
5663
organization, _ = Organization.objects.get_or_create(
5764
stack_id=instance_info["id"],
@@ -74,8 +81,13 @@ def check_gcom_permission(token_string: str, context) -> GcomToken:
7481
organization.grafana_url = instance_info["url"]
7582
organization.cluster_slug = instance_info["clusterSlug"]
7683
organization.gcom_token = token_string
77-
organization.api_token = grafana_token
7884
organization.gcom_token_org_last_time_synced = timezone.now()
85+
if not grafana_token_format_is_valid:
86+
logger.debug(
87+
f"grafana token sent when updating stack_id={stack_id} was invalid, api_token in DB will be unchanged"
88+
)
89+
else:
90+
organization.api_token = grafana_token
7991
organization.save(
8092
update_fields=[
8193
"stack_slug",
@@ -86,6 +98,7 @@ def check_gcom_permission(token_string: str, context) -> GcomToken:
8698
"gcom_token",
8799
"gcom_token_org_last_time_synced",
88100
"cluster_slug",
101+
"api_token",
89102
]
90103
)
91104
logger.debug(f"Finish authenticate by making request to gcom api for org={org_id}, stack_id={stack_id}")

engine/apps/grafana_plugin/tasks/sync_v2.py

+12-9
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,17 @@ def sync_organizations_v2(org_ids=None):
4141
orgs_per_second = math.ceil(len(organization_qs) / SYNC_PERIOD.seconds)
4242
logger.info(f"Syncing {len(organization_qs)} organizations @ {orgs_per_second} per 1s pause")
4343
for idx, org in enumerate(organization_qs):
44-
client = GrafanaAPIClient(api_url=org.grafana_url, api_token=org.api_token)
45-
_, status = client.sync()
46-
if status["status_code"] != 200:
47-
logger.error(
48-
f"Failed to request sync stack_slug={org.stack_slug} status_code={status['status_code']} url={status['url']} message={status['message']}"
49-
)
50-
if idx % orgs_per_second == 0:
51-
logger.info(f"Sleep 1s after {idx + 1} organizations processed")
52-
sleep(1)
44+
if GrafanaAPIClient.validate_grafana_token_format(org.api_token):
45+
client = GrafanaAPIClient(api_url=org.grafana_url, api_token=org.api_token)
46+
_, status = client.sync()
47+
if status["status_code"] != 200:
48+
logger.error(
49+
f"Failed to request sync stack_slug={org.stack_slug} status_code={status['status_code']} url={status['url']} message={status['message']}"
50+
)
51+
if idx % orgs_per_second == 0:
52+
logger.info(f"Sleep 1s after {idx + 1} organizations processed")
53+
sleep(1)
54+
else:
55+
logger.info(f"Skipping stack_slug={org.stack_slug}, api_token format is invalid or not set")
5356
else:
5457
logger.info(f"Issuing sync requests already in progress lock_id={lock_id}, check slow outgoing requests")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from unittest.mock import patch
2+
3+
import pytest
4+
5+
from apps.grafana_plugin.helpers.gcom import check_gcom_permission
6+
7+
8+
@pytest.mark.parametrize(
9+
"api_token, api_token_updated",
10+
[
11+
("glsa_abcdefghijklmnopqrztuvwxyz", True),
12+
("abcdefghijklmnopqrztuvwxyz", True),
13+
("abc", False),
14+
("", False),
15+
("<no_value>", False),
16+
(None, False),
17+
(24, False),
18+
],
19+
)
20+
@pytest.mark.django_db
21+
def test_check_gcom_permission_updates_fields(make_organization, api_token, api_token_updated):
22+
gcom_token = "gcom:test_token"
23+
broken_token = "broken_token"
24+
instance_info = {
25+
"id": 324534,
26+
"slug": "testinstance",
27+
"url": "http://example.com",
28+
"orgId": 5671,
29+
"orgSlug": "testorg",
30+
"orgName": "Test Org",
31+
"regionSlug": "us",
32+
"clusterSlug": "us-test",
33+
}
34+
context = {
35+
"stack_id": str(instance_info["id"]),
36+
"org_id": str(instance_info["orgId"]),
37+
"grafana_token": api_token,
38+
}
39+
40+
org = make_organization(stack_id=instance_info["id"], org_id=instance_info["orgId"], api_token=broken_token)
41+
last_time_gcom_synced = org.gcom_token_org_last_time_synced
42+
43+
with patch(
44+
"apps.grafana_plugin.helpers.GcomAPIClient.get_instance_info",
45+
return_value=instance_info,
46+
) as mock_instance_info:
47+
check_gcom_permission(gcom_token, context)
48+
mock_instance_info.assert_called()
49+
50+
org.refresh_from_db()
51+
assert org.stack_id == instance_info["id"]
52+
assert org.stack_slug == instance_info["slug"]
53+
assert org.grafana_url == instance_info["url"]
54+
assert org.org_id == instance_info["orgId"]
55+
assert org.org_slug == instance_info["orgSlug"]
56+
assert org.org_title == instance_info["orgName"]
57+
assert org.region_slug == instance_info["regionSlug"]
58+
assert org.cluster_slug == instance_info["clusterSlug"]
59+
assert org.api_token == api_token if api_token_updated else broken_token
60+
assert org.gcom_token == gcom_token
61+
assert org.gcom_token_org_last_time_synced != last_time_gcom_synced

engine/apps/grafana_plugin/tests/test_sync.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from unittest.mock import patch
1+
from unittest.mock import ANY, patch
22

33
import pytest
44
from django.conf import settings
@@ -58,8 +58,8 @@ def test_start_sync_organization_filter(make_organization):
5858
with patch("apps.grafana_plugin.tasks.sync.sync_organization_async.apply_async") as mock_sync:
5959
start_sync_organizations()
6060
assert mock_sync.call_count == 2
61-
mock_sync.assert_any_call((org2.pk,), countdown=0)
62-
mock_sync.assert_any_call((org3.pk,), countdown=1)
61+
mock_sync.assert_any_call((org2.pk,), countdown=ANY)
62+
mock_sync.assert_any_call((org3.pk,), countdown=ANY)
6363

6464

6565
@pytest.mark.django_db

engine/apps/grafana_plugin/tests/test_sync_v2.py

+29
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from rest_framework.test import APIClient
77

88
from apps.api.permissions import LegacyAccessControlRole
9+
from apps.grafana_plugin.tasks import sync_organizations_v2
910

1011

1112
@pytest.mark.django_db
@@ -44,3 +45,31 @@ def test_invalid_auth(make_organization_and_user_with_plugin_token, make_user_au
4445

4546
assert response.status_code == status.HTTP_401_UNAUTHORIZED
4647
assert not mock_sync.called
48+
49+
50+
@pytest.mark.parametrize(
51+
"api_token, sync_called",
52+
[
53+
("", False),
54+
("abc", False),
55+
("glsa_abcdefghijklmnopqrstuvwxyz", True),
56+
],
57+
)
58+
@pytest.mark.django_db
59+
def test_skip_org_without_api_token(make_organization, api_token, sync_called):
60+
organization = make_organization(api_token=api_token)
61+
62+
with patch(
63+
"apps.grafana_plugin.helpers.GrafanaAPIClient.sync",
64+
return_value=(
65+
None,
66+
{
67+
"url": "",
68+
"connected": True,
69+
"status_code": status.HTTP_200_OK,
70+
"message": "",
71+
},
72+
),
73+
) as mock_sync:
74+
sync_organizations_v2(org_ids=[organization.id])
75+
assert mock_sync.called == sync_called

grafana-plugin/go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/grafana-labs/grafana-oncall-app
22

3-
go 1.21
3+
go 1.21.5
44

55
require github.com/grafana/grafana-plugin-sdk-go v0.228.0
66

grafana-plugin/src/utils/consts.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,18 @@ export const getProcessEnvVarSafely = (name: string) => {
7272
}
7373
};
7474

75-
export const getOnCallApiPath = (subpath = '') => `/api/plugins/${PLUGIN_ID}/resources${subpath}`;
75+
const getGrafanaSubUrl = () => {
76+
try {
77+
return window.grafanaBootData.settings.appSubUrl || '';
78+
} catch (_err) {
79+
return '';
80+
}
81+
};
82+
83+
export const getOnCallApiPath = (subpath = '') => {
84+
// We need to consider the grafanaSubUrl in case Grafana is served from subpath, e.g. http://localhost:3000/grafana
85+
return `${getGrafanaSubUrl()}/api/plugins/${PLUGIN_ID}/resources${subpath}`;
86+
};
7687

7788
// Faro
7889
export const FARO_ENDPOINT_DEV =

0 commit comments

Comments
 (0)