Merge branch 'main' into laura/feat/validate-target-query

This commit is contained in:
Laura Benz
2023-01-03 14:39:25 +01:00
63 changed files with 1525 additions and 503 deletions
+4 -7
View File
@@ -11,17 +11,14 @@ exports[`no enzyme tests`] = {
"packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/ViewingLayer.test.tsx:793800575": [
[14, 35, 13, "RegExp match", "2409514259"]
],
"packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/index.test.tsx:596989456": [
[14, 35, 13, "RegExp match", "2409514259"]
],
"packages/jaeger-ui-components/src/TraceTimelineViewer/ListView/index.test.tsx:3266788928": [
[14, 56, 13, "RegExp match", "2409514259"]
],
"packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.test.js:989353473": [
[15, 17, 13, "RegExp match", "2409514259"]
"packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.test.tsx:1220382119": [
[15, 31, 13, "RegExp match", "2409514259"]
],
"packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineHeaderRow/TimelineViewingLayer.test.js:4192868446": [
[15, 17, 13, "RegExp match", "2409514259"]
"packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineHeaderRow/TimelineViewingLayer.test.tsx:3450948735": [
[15, 31, 13, "RegExp match", "2409514259"]
],
"packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.test.js:551014442": [
[13, 26, 13, "RegExp match", "2409514259"]
+35 -1
View File
@@ -7,7 +7,7 @@ Useful for testing a second implementation
## Currently configured in DB and instructions
- [x] LDAP
- [ ] SAML
- [x] SAML
- [ ] OAuth
## Deployment
@@ -38,6 +38,40 @@ sync_cron = "* * * * 1"
active_sync_enabled = true
```
## SAML Setup
**Warning:** SLO
Grafana expects SLO support to be communicated in the metadata
Single Logout is not supported by authentik.
https://github.com/goauthentik/authentik/issues/3321
**Warning** Client signature validation
Grafana expects the idP to retrieve the client's public key from the metadata.
Authentik does not seem to support this and therefore client signature verification is set
as optional.
```ini
[auth.saml]
enabled = true
certificate_path = devenv/docker/blocks/auth/authentik/cert.crt
private_key_path = devenv/docker/blocks/auth/authentik/key.pem
idp_metadata_url = http://localhost:9000/api/v3/providers/saml/2/metadata/?download
assertion_attribute_name = http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name
assertion_attribute_login = http://schemas.goauthentik.io/2021/02/saml/username
assertion_attribute_email = http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress
assertion_attribute_groups = http://schemas.xmlsoap.org/claims/Group
assertion_attribute_org = http://schemas.xmlsoap.org/claims/Group
allow_sign_up = true
single_logout = false # not supported by authentik
signature_algorithm = rsa-sha256
allow_idp_initiated = true
org_mapping = admin:1:Admin, editor:1:Editor, viewer:1:Viewer
```
## Available users and groups
*authentik admin*:
@@ -0,0 +1,32 @@
-----BEGIN CERTIFICATE-----
MIIFmTCCA4GgAwIBAgIURahpP+wtYqe6MePw3wj3fKPtEkQwDQYJKoZIhvcNAQEL
BQAwXDELMAkGA1UEBhMCVVMxDzANBgNVBAgMBkRlbmlhbDEUMBIGA1UEBwwLU3By
aW5nZmllbGQxDDAKBgNVBAoMA0RpczEYMBYGA1UEAwwPd3d3LmV4YW1wbGUuY29t
MB4XDTIyMDcwODA3MDYwOFoXDTIzMDcwODA3MDYwOFowXDELMAkGA1UEBhMCVVMx
DzANBgNVBAgMBkRlbmlhbDEUMBIGA1UEBwwLU3ByaW5nZmllbGQxDDAKBgNVBAoM
A0RpczEYMBYGA1UEAwwPd3d3LmV4YW1wbGUuY29tMIICIjANBgkqhkiG9w0BAQEF
AAOCAg8AMIICCgKCAgEAzkEB7WRDIMv1iX+4KmDJKPFCCJhZK+UNULCEFmpV/9+I
0vHdj55eVj2fuxahuemWdC+GogXL+ybAPhLvJwnl5OlojH9uco7IuQI/D8C14IwK
/qtvBouhCkbwl/+s3GGDqy7dmFk3O9OmllSADlIpFRSioEc6l/8sshfelsyXXv+5
kKu87CRYDTLdC/Mhxa5dA1zqUZnvvrfeuc41Z8Aj3kFNSXeRbMKA+ejm0kA+Bc+M
XXhP4T4gBuk1gR0Bo0y2sEDb0mlJ1H/FGkJZ6n12gj3IBYMfrfJwj00PVUvXAq/8
rtfieKgZvcrbwR7fJ7upaWROuxmknjGJPh11Hzm4d653M34BkDNYMr/e49Kktuv7
vDG8KOHx7f16r3Ze8CL1NYy4DXicoplZo/pdyPYIrdt1bPMZ4fwqufgPORVZm3/z
2JDpeHY2DtqP14jFDW5fYzdJ4Zt8YZgtAzuBbZB4QMFoOK+9GkfZMz/8LtProEKt
dhEDksygIbPmyfuyI7tJYtJ4Y28Ybw2nE8XJ3SR1zyhvjCTgQo063xBgPiADVD1V
cvpnt3n1/5UwgtLSJzVLu8az+Hj4iq7RjeYdmwGFHeccRZv/u8dNmjqMkt9URGcn
cps8s4pN3VJeqQX4/G6n24CoBy3Nzu2sGgSnG6UOkVGc9WobjPRrKzdpnxm+ercC
AwEAAaNTMFEwHQYDVR0OBBYEFA7V5G5JiS51iEZLL1cxgJdt0y2EMB8GA1UdIwQY
MBaAFA7V5G5JiS51iEZLL1cxgJdt0y2EMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI
hvcNAQELBQADggIBADDcBZSlux/RR1o/hnr9dxqYghVXdoM1Mnjuq0A4go+GbzE9
t4/ihdyzIhtDgIwZJUBma6OzoZgzH/0W+iOFKZLpGg3i5LizbN8Ry0ndRq4ZbjZ2
8nwwSTAh2RkO8XZglrKrYyHm4nT2WPg17x2u1cQTy/t/jtE/EdULY9Pu8pUuw2MS
J4B0zU2r7N/+PmwzkC7RD+9zhQKY/tHMvwOdFgU6aeunzNtUAE4YOjhhM0UF1njt
RAhXjPEi39wbE7RYurcCoUZD21B5YMq9j8DG5emgiEa2ygcIxKJAzJ9ESzaXCehS
FNSp/LEI8PQLpN/nDh4lBZZFvVPmm+/BEonLtR2qr7fuWNZX/pQIv+hLRrXy9AF0
8/fAG4+Te07Kz7Zoz24Dsyth66ReX4hodjAvJkYzbTFiuBcxMZSXrdgBWIpGXboY
4hxZjem3qUo7IVsbEToptEh4rF8xs7uTk8+xtnjgQY62mDWaDCudpVCfaL33SnX0
FRyzz9TI5meOrNRAhKpuaDLit+v3C2NAKPo3Yi9VLTY0eunYPlgNDrKZ2JNNlTxw
dk1vM2/mq1hhGgbzixmRZpvc7g/H7t6mOVqKg+U0XC78tCjZO0UY3TGwjMmCw8i/
qG0nX8quZ/hYyf6dAMDFw4Rw9BTW6vkanRJz0/v7mxuyJIoJ6B6jRvLWTzbw
-----END CERTIFICATE-----
+69 -26
View File
@@ -2,8 +2,8 @@
-- PostgreSQL database dump
--
-- Dumped from database version 12.12
-- Dumped by pg_dump version 12.12
-- Dumped from database version 12.13
-- Dumped by pg_dump version 12.13
SET statement_timeout = 0;
SET lock_timeout = 0;
@@ -2497,6 +2497,7 @@ COPY public.authentik_blueprints_blueprintinstance (created, last_updated, manag
COPY public.authentik_core_application (policybindingmodel_ptr_id, name, slug, meta_launch_url, meta_description, meta_publisher, provider_id, meta_icon, "group", open_in_new_tab) FROM stdin;
77d294b5-8a50-4d6d-8edd-5b15e5871826 Grafana grafana 1 t
506b1180-4069-4dda-9405-e185f355c34d Grafana SAML grafana-saml 2 t
\.
@@ -2505,13 +2506,8 @@ COPY public.authentik_core_application (policybindingmodel_ptr_id, name, slug, m
--
COPY public.authentik_core_authenticatedsession (expires, expiring, uuid, session_key, last_ip, last_user_agent, last_used, user_id) FROM stdin;
2022-11-08 09:17:04.214445+00 t 5e425054-99e2-42a5-825b-6f91340c588d c62hjlwggmigklbysnjtp27ngqnnpbet 192.168.48.1 Mozilla/5.0 (X11; Linux x86_64; rv:106.0) Gecko/20100101 Firefox/106.0 2022-10-25 09:17:04.214738+00 1
2022-11-08 09:53:30.259637+00 t 0b7411fe-871e-4936-84ec-03cd5955dc71 jeye9zabh725eoyreourgd4exswni4hz 192.168.48.1 goauthentik.io/outpost/2022.10.0 2022-10-25 09:53:30.259801+00 6
2022-11-08 09:53:37.37657+00 t 959492a1-cf7e-40d3-8619-793981ce2226 8uc0fdpimytzcilq3tb53nsjpzjfp6x6 192.168.48.1 goauthentik.io/outpost/2022.10.0 2022-10-25 09:53:37.376711+00 6
2022-11-08 09:53:47.038795+00 t 030b257a-ac48-4695-8d31-05ab564cc005 hevohjgfg5iptplcsylpfxlrifwn3l7q 192.168.48.1 goauthentik.io/outpost/2022.10.0 2022-10-25 09:53:47.03893+00 6
2022-11-08 09:53:47.787505+00 t 6d327213-04a9-4ff3-8117-5f29157020fa dp93085lk6qggac9wo1qw4k7p8iih95g 192.168.48.1 goauthentik.io/outpost/2022.10.0 2022-10-25 09:53:47.787698+00 6
2022-11-08 09:54:00.199533+00 t 90cb55c3-56d4-4310-8516-549284316c46 02oy1th515jtb7atyhocmfusdjx5oczl 192.168.48.1 goauthentik.io/outpost/2022.10.0 2022-10-25 09:54:00.199672+00 6
2022-11-08 09:54:00.960305+00 t a203616f-cf71-4b30-88fb-440cb75d18eb wqs1feozyr605vl93efmaiaqt999abiy 192.168.48.1 goauthentik.io/outpost/2022.10.0 2022-10-25 09:54:00.960442+00 5
2023-01-16 14:16:55.806987+00 t b7327320-c234-46b6-a1ee-05ca2d8e6871 zhvx6kegjga3td4qm1baw0nl6v6vidqd 172.24.0.1 Mozilla/5.0 (X11; Linux x86_64; rv:108.0) Gecko/20100101 Firefox/108.0 2023-01-02 14:16:55.807308+00 1
2023-01-16 16:38:22.075322+00 t f785c0cb-0a2c-4b03-8f0d-5577684f3e8e av8uhyv2bz1v0g09wti608pzujwin3fh 172.24.0.1 Mozilla/5.0 (X11; Linux x86_64; rv:108.0) Gecko/20100101 Firefox/108.0 2023-01-02 16:38:22.07557+00 5
\.
@@ -2525,6 +2521,7 @@ a9c6327c-cfca-4d5b-a0af-421645e43c31 ldapsearch {} \N f
17bb354e-cea2-48db-b322-9f9c8b53b8bb admin {} \N f
7e216c1b-2a0f-4772-ac3d-7bdaf7511a2e editor {} \N f
10d9a55e-195b-41a0-9fe6-63382d5b8208 viewer {} \N f
ca14281c-e025-4691-b5a2-3ee415920e80 extra-group {} \N f
\.
@@ -2561,6 +2558,7 @@ e8809202-4ff0-4086-b078-196e6152e826 authentik default OpenLDAP Mapping: cn retu
COPY public.authentik_core_provider (id, authorization_flow_id, name) FROM stdin;
1 87ca9599-5323-451c-8603-2f5bee7a0c12 grafana-ldap
2 a7ef54bb-7959-4559-979d-9345c022e086 grafana-saml
\.
@@ -2569,6 +2567,13 @@ COPY public.authentik_core_provider (id, authorization_flow_id, name) FROM stdin
--
COPY public.authentik_core_provider_property_mappings (id, provider_id, propertymapping_id) FROM stdin;
1 2 05712cf6-0557-4ae7-95e7-3a0538962b67
2 2 38ac59c3-e754-44e7-9790-5108a2cd027c
3 2 ef027c1a-728f-44e5-9072-6942d3e8d15e
4 2 29fab284-b6db-433a-9c3c-7575aba47b2b
5 2 c5d93ce6-8cda-42fa-9c19-3837ba67788a
6 2 765b25ee-0e8b-4394-a56c-e27abf68f5ce
7 2 b3fa1ee7-9507-4869-a29a-3c17c2482839
\.
@@ -2606,12 +2611,12 @@ COPY public.authentik_core_token (token_uuid, expires, expiring, description, us
COPY public.authentik_core_user (id, password, last_login, username, first_name, last_name, email, is_active, date_joined, uuid, name, password_change_date, attributes, path) FROM stdin;
2 !n7aLj0jKk3WIaSvfbaqM1nyy6fWejHsM54lRFljX \N AnonymousUser t 2022-10-25 09:01:03.467789+00 fe23113c-e658-40b0-872b-d522b6d84e95 2022-10-25 09:01:03.468042+00 {} users
7 pbkdf2_sha256$390000$s67BmqSPGYWQPDnZ01phY4$ky7qvH0SBsnsqeEM19AB0B4Ki8zIcmfshf7/r7aiodE= \N authentik-viewer authentik-viewer@localhost t 2022-10-25 09:56:58.979268+00 ee7aa163-4303-4c2d-99c7-293ffd9d08dd authentik-viewer 2022-10-25 09:57:13.55852+00 {} users
1 pbkdf2_sha256$390000$c4McOkDtg2Hps9uyXGdezW$8g7sh3praXa6Jo1AHfrng1dsf8dktric2hQYlUyAGyY= 2022-10-25 09:17:04.22378+00 akadmin admin@localhost t 2022-10-25 09:00:53.707692+00 d719cb26-008d-4bff-8f5c-13d1bf53de25 authentik Default Admin 2022-10-25 09:09:42.841946+00 {} users
8 pbkdf2_sha256$390000$g9w4rydmb2SLrzWYyInNZZ$W5G+Y2iayJp1KIp9wHuiozJ79Mdibo3zFx+Zxvofm5s= \N authentik-editor authentik-editor@localhost t 2022-10-25 09:57:43.64112+00 3678963f-023e-48b3-97a1-0910b4fbbfea authentik-editor 2022-10-25 09:57:54.40015+00 {} users
4 !yrI1uXKCdF0svOmz83ldwXJnWMOUY8oY52dzYNT1 \N ak-outpost-efe635b92ce74de4977f9f25b9f36d97 t 2022-10-25 09:13:37.843347+00 a7dc29a9-11b7-4d35-aec1-260ae1ea62f5 Outpost ldap-outpost Service-Account 2022-10-25 09:13:37.843557+00 {"goauthentik.io/user/override-ips": true, "goauthentik.io/user/service-account": true} goauthentik.io/outposts
3 !8j4wiqLzHmjS9fUEU0e0DsXx4z8Cj8W6lXnOCbo3 \N ak-outpost-0c24aadcf97e4720a70f72a190b0cafc t 2022-10-25 09:01:07.984838+00 b9835bf9-2711-41ef-86d2-b07124b9fef9 Outpost authentik Embedded Outpost Service-Account 2022-10-25 09:01:07.98507+00 {"goauthentik.io/user/override-ips": true, "goauthentik.io/user/service-account": true} goauthentik.io/outposts
6 pbkdf2_sha256$390000$k0Ykknoutfj56D2AaJFxWJ$bmAmMoNISoYnUn+DQnepRd39Mp1pnY74yfAoiHwRYRc= 2022-10-25 09:54:00.207978+00 ldapservice ldapservice@localhost t 2022-10-25 09:49:18.218809+00 94fafa12-9abd-4567-9627-3c6998f38cca ldapservice 2022-10-25 09:49:50.76013+00 {} users
5 pbkdf2_sha256$390000$GmvsVEytoLZGHoKB8LdomP$Xi06ESYoLZ/ijL0FZDT964KM9C9WoPhQtv6Rfl3b6Nc= 2022-10-25 09:54:00.969255+00 authentik-admin authentik-admin@localhost t 2022-10-25 09:37:30.689793+00 a7d3e4ed-8b75-456f-b3aa-f53f45afd3a4 authentik-admin 2022-10-25 09:37:49.480346+00 {} users
1 pbkdf2_sha256$390000$c4McOkDtg2Hps9uyXGdezW$8g7sh3praXa6Jo1AHfrng1dsf8dktric2hQYlUyAGyY= 2023-01-02 14:16:55.816269+00 akadmin admin@localhost t 2022-10-25 09:00:53.707692+00 d719cb26-008d-4bff-8f5c-13d1bf53de25 authentik Default Admin 2022-10-25 09:09:42.841946+00 {} users
5 pbkdf2_sha256$390000$GmvsVEytoLZGHoKB8LdomP$Xi06ESYoLZ/ijL0FZDT964KM9C9WoPhQtv6Rfl3b6Nc= 2023-01-02 16:38:22.084529+00 authentik-admin authentik-admin@localhost t 2022-10-25 09:37:30.689793+00 a7d3e4ed-8b75-456f-b3aa-f53f45afd3a4 authentik-admin 2022-10-25 09:37:49.480346+00 {} users
3 !8j4wiqLzHmjS9fUEU0e0DsXx4z8Cj8W6lXnOCbo3 \N ak-outpost-0c24aadcf97e4720a70f72a190b0cafc t 2022-10-25 09:01:07.984838+00 b9835bf9-2711-41ef-86d2-b07124b9fef9 Outpost authentik Embedded Outpost Service-Account 2022-10-25 09:01:07.98507+00 {"goauthentik.io/user/override-ips": true, "goauthentik.io/user/service-account": true} goauthentik.io/outposts
4 !yrI1uXKCdF0svOmz83ldwXJnWMOUY8oY52dzYNT1 \N ak-outpost-efe635b92ce74de4977f9f25b9f36d97 t 2022-10-25 09:13:37.843347+00 a7dc29a9-11b7-4d35-aec1-260ae1ea62f5 Outpost ldap-outpost Service-Account 2022-10-25 09:13:37.843557+00 {"goauthentik.io/user/override-ips": true, "goauthentik.io/user/service-account": true} goauthentik.io/outposts
\.
@@ -2625,6 +2630,8 @@ COPY public.authentik_core_user_ak_groups (id, user_id, group_id) FROM stdin;
3 5 17bb354e-cea2-48db-b322-9f9c8b53b8bb
4 8 7e216c1b-2a0f-4772-ac3d-7bdaf7511a2e
5 7 10d9a55e-195b-41a0-9fe6-63382d5b8208
6 8 ca14281c-e025-4691-b5a2-3ee415920e80
7 5 ca14281c-e025-4691-b5a2-3ee415920e80
\.
@@ -2641,10 +2648,10 @@ COPY public.authentik_core_user_groups (id, user_id, group_id) FROM stdin;
--
COPY public.authentik_core_user_user_permissions (id, user_id, permission_id) FROM stdin;
2 3 21
28 4 21
29 4 321
30 4 348
31 4 21
32 4 321
33 4 348
34 3 21
\.
@@ -2701,6 +2708,7 @@ a4594d02-c239-4607-b95b-3043d3d3b565 login_failed authentik.events.signals {"sta
ede37431-b8b6-4b88-baaf-78a58802305c login_failed authentik.events.signals {"stage": {"pk": "02b4a829f9ab45d0adcc11e3c165f1c4", "app": "authentik_stages_password", "name": "default-authentication-password", "model_name": "passwordstage"}, "password": "********************", "username": "akadmin", "http_request": {"args": {"query": "goauthentik.io%2Foutpost%2Fldap=true"}, "path": "/api/v3/flows/executor/default-authentication-flow/", "method": "POST"}} 192.168.48.1 2022-10-25 09:42:31.137414+00 {"pk": 2, "email": "", "username": "AnonymousUser"} 2023-10-25 09:42:31.132685+00 t {"pk": "1b45e4ae6c1b4ea89691df11c97baeef", "app": "authentik_tenants", "name": "Default tenant", "model_name": "tenant"}
8d8dab83-750e-44f3-b5f9-e33b85e2f8b2 model_updated authentik.events.signals {"model": {"pk": 1, "app": "authentik_providers_ldap", "name": "grafana-ldap", "model_name": "ldapprovider"}, "http_request": {"args": {}, "path": "/api/v3/providers/ldap/1/", "method": "PUT"}} 192.168.48.1 2022-10-25 09:43:27.594287+00 {"pk": 1, "email": "admin@localhost", "username": "akadmin"} 2023-10-25 09:43:27.59388+00 t {"pk": "1b45e4ae6c1b4ea89691df11c97baeef", "app": "authentik_tenants", "name": "Default tenant", "model_name": "tenant"}
dacab0a4-45f4-43be-8471-44d99f3b8f4f login_failed authentik.events.signals {"stage": {"pk": "02b4a829f9ab45d0adcc11e3c165f1c4", "app": "authentik_stages_password", "name": "default-authentication-password", "model_name": "passwordstage"}, "password": "********************", "username": "akadmin", "http_request": {"args": {"query": "goauthentik.io%2Foutpost%2Fldap=true"}, "path": "/api/v3/flows/executor/default-authentication-flow/", "method": "POST"}} 192.168.48.1 2022-10-25 09:43:30.63453+00 {"pk": 2, "email": "", "username": "AnonymousUser"} 2023-10-25 09:43:30.629991+00 t {"pk": "1b45e4ae6c1b4ea89691df11c97baeef", "app": "authentik_tenants", "name": "Default tenant", "model_name": "tenant"}
12d75a37-20ee-44f4-9f1b-9f99d6dfdeea update_available authentik.admin.tasks {"message": "Changelog: https://goauthentik.io/docs/releases/2022.12#fixed-in-2022121", "new_version": "2022.12.1"} \N 2023-01-02 14:17:08.672486+00 {} 2024-01-02 14:17:08.671057+00 t {"pk": "3fe5e3c1df6f42e1a4a934178957c3b6", "app": "authentik_tenants", "name": "Tenant fallback", "model_name": "tenant"}
63133b06-3f6d-4b91-8dab-3de201e71ab5 model_updated authentik.events.signals {"model": {"pk": 1, "app": "authentik_providers_ldap", "name": "grafana-ldap", "model_name": "ldapprovider"}, "http_request": {"args": {}, "path": "/api/v3/providers/ldap/1/", "method": "PUT"}} 192.168.48.1 2022-10-25 09:44:12.026999+00 {"pk": 1, "email": "admin@localhost", "username": "akadmin"} 2023-10-25 09:44:12.026611+00 t {"pk": "1b45e4ae6c1b4ea89691df11c97baeef", "app": "authentik_tenants", "name": "Default tenant", "model_name": "tenant"}
8b1c946c-4713-47ff-a104-9053e97e4a70 model_created authentik.events.signals {"model": {"pk": "f4abcb7d5f9248908760afb9effda665", "app": "authentik_stages_identification", "name": "ldap-identification-stage", "model_name": "identificationstage"}, "http_request": {"args": {}, "path": "/api/v3/stages/identification/", "method": "POST"}} 192.168.48.1 2022-10-25 09:45:15.819069+00 {"pk": 1, "email": "admin@localhost", "username": "akadmin"} 2023-10-25 09:45:15.81859+00 t {"pk": "1b45e4ae6c1b4ea89691df11c97baeef", "app": "authentik_tenants", "name": "Default tenant", "model_name": "tenant"}
bcd2fae1-ef0b-4f9c-807d-67a66817d063 model_created authentik.events.signals {"model": {"pk": "95715d75025b4e5e9d2396826479906d", "app": "authentik_stages_password", "name": "ldap-authentication-password", "model_name": "passwordstage"}, "http_request": {"args": {}, "path": "/api/v3/stages/password/", "method": "POST"}} 192.168.48.1 2022-10-25 09:45:43.056738+00 {"pk": 1, "email": "admin@localhost", "username": "akadmin"} 2023-10-25 09:45:43.056256+00 t {"pk": "1b45e4ae6c1b4ea89691df11c97baeef", "app": "authentik_tenants", "name": "Default tenant", "model_name": "tenant"}
@@ -2715,6 +2723,7 @@ dfab21a9-ad29-4980-a762-80a1f0c42ab5 model_created authentik.events.signals {"mo
d273c74c-ec69-445b-87aa-0a7869c359b6 password_set authentik.events.signals {} 255.255.255.255 2022-10-25 09:49:44.127166+00 {"pk": 6, "email": "ldapservice@localhost", "username": "ldapservice"} 2023-10-25 09:49:44.12686+00 t {"pk": "124e241c825640d9b416596b2daab8b3", "app": "authentik_tenants", "name": "Tenant fallback", "model_name": "tenant"}
9969bf93-4d3f-4325-97db-4037c6e9ed59 model_updated authentik.events.signals {"model": {"pk": 6, "app": "authentik_core", "name": "ldapservice", "model_name": "user"}, "http_request": {"args": {}, "path": "/api/v3/core/users/6/set_password/", "method": "POST"}} 192.168.48.1 2022-10-25 09:49:44.253791+00 {"pk": 1, "email": "admin@localhost", "username": "akadmin"} 2023-10-25 09:49:44.253274+00 t {"pk": "1b45e4ae6c1b4ea89691df11c97baeef", "app": "authentik_tenants", "name": "Default tenant", "model_name": "tenant"}
f8e8270f-871a-41bd-b02e-f3c29e9897f8 password_set authentik.events.signals {} 255.255.255.255 2022-10-25 09:49:45.195476+00 {"pk": 6, "email": "ldapservice@localhost", "username": "ldapservice"} 2023-10-25 09:49:45.195113+00 t {"pk": "669e359eaf174055957065109edc72a8", "app": "authentik_tenants", "name": "Tenant fallback", "model_name": "tenant"}
a36d1d0c-a444-4dcd-b27c-297ecfce47fc authorize_application authentik.providers.saml.views.flows {"flow": "a7ef54bb79594559979d9345c022e086", "http_request": {"args": {"query": "SAMLRequest=nJJBj9MwEIX%2FiuV7aicNSWttIpWtEJUWqLaFA7epM9lacuzgmQD771GzRVok1MMebc97n%2BfN3BEMfjSbic%2FhEX9MSCx%2BDz6QuTw0ckrBRCBHJsCAZNiaw%2BbTgykW2gARJnYxyFeS8bZmTJGjjV6K3baRrstWuu7zWndlX%2BaAuF6eynVV9brP39WnWq9sUeVVibUU3zCRi6GRxUJLsSOacBeIIXAjC10sM51nujjmlVnmZrlerMr6uxRbJHYBeFaemUejlI8W%2FDkSm7XWWsE4emfnEnVpQT0l6CFANh%2BIojq50LnwpBJ2LqFlJcXmb%2FP3MdA0YDpg%2Buksfn18%2BA9neeHMdmBJiv01hfcvvrcju8LJfDwe99n%2By%2BEo23lsZs4giQ8xDcC3TS43rsv6udRgYMfPsr3xzwEZOmC4U69Q7XVdPsOAu%2B0%2Bemef34DnBIEcBpZi4338dZ8QGBvJaUKp2hfkv0vZ%2FgkAAP%2F%2F&RelayState=TUVNeGt5N3FINDA5UGVEM1JuSHA1dU82TFBWUUZPSzVEcFR2U2RqQ1hrb2lOdmdKMEp8U2RLaHBycFZ6&SigAlg=http%3A%2F%2Fwww.w3.org%2F2001%2F04%2Fxmldsig-more%23rsa-sha256&Signature=w3j4ku9na9TCOBB0lF%2FHA5VTsVQb%2Fa0kuSgILzVcbLTq%2BcAcPRNmEcpVqZPHcWtIfJPw%2F%2FP0HsQhKiPm70JbrVBbZPsZO3ysTe%2Fa9bhpot1yf9HHbkFWShcT%2BA5hgap1DEvJMSZD8N5%2Fkq%2BO47s9%2F0ngZeN%2FErO41mXDrvEGXFEJfHCgU18BOAwbKN9eGj09idnldNiUJ4mm1HEbAHI935KS6buBDnLgM0dYIYZzEIBKewt7j8poD98hRyV9GnCBrN%2FH2D7yOf3IGwKBc4hdMhYbeqBKcj4MTywBEQIc1uY9nUdtRI4N1Nrfmwd2KOpribVB%2FE6musXjb%2Fxo5%2BRmvyMASQGb%2Foo1QIfDZ87lRbRNKffw8KTI7dTusJTl%2BH6mYEtWU5rd%2B6bsiSLFiLl%2F7wpFOHlynTaqte2J1Cg7RAaJt8o0V7vb3%2BpS9CxaFY%2BF8J7MLc3drOTLy9EReBgV2vmdkwS2lE92EYkOzm987nusuUGP2f8ET5oUr1udHw1w2UPdyoJHdTYiHwLUufCM1%2BgaJsoZALfzgW0vKe8ghMUrHl03M9mddynTF%2B%2F9y%2FtIp3INVoWeS6WRHhX0bwlkwBXpL675KkW7q%2F0rVYJ3m6BOt9JkzXPv3l42yQPYs8KY3TXvbwIf2m0L%2FTORX5Jnhs09rWhQrNv%2BrONgD1mUrsA%3D"}, "path": "/api/v3/flows/executor/default-provider-authorization-implicit-consent/", "method": "GET"}, "authorized_application": {"pk": "506b118040694dda9405e185f355c34d", "app": "authentik_core", "name": "Grafana SAML", "model_name": "application"}} 172.24.0.1 2023-01-02 16:31:40.277118+00 {"pk": 1, "email": "admin@localhost", "username": "akadmin"} 2024-01-02 16:31:40.276431+00 t {"pk": "1b45e4ae6c1b4ea89691df11c97baeef", "app": "authentik_tenants", "name": "Default tenant", "model_name": "tenant"}
c07b9726-c9a6-41d9-8750-298dc6c02bb9 model_updated authentik.events.signals {"model": {"pk": 6, "app": "authentik_core", "name": "ldapservice", "model_name": "user"}, "http_request": {"args": {}, "path": "/api/v3/core/users/6/set_password/", "method": "POST"}} 192.168.48.1 2022-10-25 09:49:45.334164+00 {"pk": 1, "email": "admin@localhost", "username": "akadmin"} 2023-10-25 09:49:45.333246+00 t {"pk": "1b45e4ae6c1b4ea89691df11c97baeef", "app": "authentik_tenants", "name": "Default tenant", "model_name": "tenant"}
0e5513c2-7946-4809-b7ad-e8ee39eb06f7 password_set authentik.events.signals {} 255.255.255.255 2022-10-25 09:49:50.747043+00 {"pk": 6, "email": "ldapservice@localhost", "username": "ldapservice"} 2023-10-25 09:49:50.746711+00 t {"pk": "669e359eaf174055957065109edc72a8", "app": "authentik_tenants", "name": "Tenant fallback", "model_name": "tenant"}
3c0fb3f6-a496-4f75-8748-065b52ecef2d model_updated authentik.events.signals {"model": {"pk": 6, "app": "authentik_core", "name": "ldapservice", "model_name": "user"}, "http_request": {"args": {}, "path": "/api/v3/core/users/6/set_password/", "method": "POST"}} 192.168.48.1 2022-10-25 09:49:50.876408+00 {"pk": 1, "email": "admin@localhost", "username": "akadmin"} 2023-10-25 09:49:50.87598+00 t {"pk": "1b45e4ae6c1b4ea89691df11c97baeef", "app": "authentik_tenants", "name": "Default tenant", "model_name": "tenant"}
@@ -2744,6 +2753,33 @@ a8885c27-5608-4eb5-a0d1-1f19dea20bda model_created authentik.events.signals {"mo
3454a52b-238c-4cb6-8db5-64c8303e339c model_updated authentik.events.signals {"model": {"pk": "17bb354ecea248dbb3229f9c8b53b8bb", "app": "authentik_core", "name": "admin", "model_name": "group"}, "http_request": {"args": {}, "path": "/api/v3/core/groups/17bb354e-cea2-48db-b322-9f9c8b53b8bb/", "method": "PUT"}} 192.168.48.1 2022-10-25 09:59:21.181345+00 {"pk": 1, "email": "admin@localhost", "username": "akadmin"} 2023-10-25 09:59:21.180942+00 t {"pk": "1b45e4ae6c1b4ea89691df11c97baeef", "app": "authentik_tenants", "name": "Default tenant", "model_name": "tenant"}
fdf81360-a8a8-4c8f-988d-2ed85c613a49 model_updated authentik.events.signals {"model": {"pk": "7e216c1b2a0f4772ac3d7bdaf7511a2e", "app": "authentik_core", "name": "editor", "model_name": "group"}, "http_request": {"args": {}, "path": "/api/v3/core/groups/7e216c1b-2a0f-4772-ac3d-7bdaf7511a2e/", "method": "PUT"}} 192.168.48.1 2022-10-25 09:59:30.4796+00 {"pk": 1, "email": "admin@localhost", "username": "akadmin"} 2023-10-25 09:59:30.478673+00 t {"pk": "1b45e4ae6c1b4ea89691df11c97baeef", "app": "authentik_tenants", "name": "Default tenant", "model_name": "tenant"}
a3998ed2-5beb-4a26-b395-f5b579d632ca model_updated authentik.events.signals {"model": {"pk": "10d9a55e195b41a09fe663382d5b8208", "app": "authentik_core", "name": "viewer", "model_name": "group"}, "http_request": {"args": {}, "path": "/api/v3/core/groups/10d9a55e-195b-41a0-9fe6-63382d5b8208/", "method": "PUT"}} 192.168.48.1 2022-10-25 09:59:42.963339+00 {"pk": 1, "email": "admin@localhost", "username": "akadmin"} 2023-10-25 09:59:42.962914+00 t {"pk": "1b45e4ae6c1b4ea89691df11c97baeef", "app": "authentik_tenants", "name": "Default tenant", "model_name": "tenant"}
2b3c7de7-f3cb-4e4d-bbcd-36ff4d1add8e login authentik.events.signals {"auth_method": "password", "http_request": {"args": {"query": "next=%2F"}, "path": "/api/v3/flows/executor/default-authentication-flow/", "method": "GET"}, "auth_method_args": {}} 172.24.0.1 2023-01-02 14:16:55.819839+00 {"pk": 1, "email": "admin@localhost", "username": "akadmin"} 2024-01-02 14:16:55.819267+00 t {"pk": "1b45e4ae6c1b4ea89691df11c97baeef", "app": "authentik_tenants", "name": "Default tenant", "model_name": "tenant"}
eeb1be95-4ade-4812-93b0-2c87db29716c update_available authentik.admin.tasks {"message": "Changelog: https://goauthentik.io/docs/releases/2022.12#fixed-in-2022121", "new_version": "2022.12.1"} \N 2023-01-02 14:17:08.645256+00 {} 2024-01-02 14:17:08.643976+00 t {"pk": "3fe5e3c1df6f42e1a4a934178957c3b6", "app": "authentik_tenants", "name": "Tenant fallback", "model_name": "tenant"}
fa558ccb-705b-43b8-9a2e-86d4f1d8d0a0 system_task_exception authentik.events.monitored_tasks {"message": "Task notification_transport encountered an error: Traceback (most recent call last):\\n File \\"/usr/local/lib/python3.10/site-packages/celery/app/trace.py\\", line 451, in trace_task\\n R = retval = fun(*args, **kwargs)\\n File \\"/usr/local/lib/python3.10/site-packages/sentry_sdk/integrations/celery.py\\", line 207, in _inner\\n reraise(*exc_info)\\n File \\"/usr/local/lib/python3.10/site-packages/sentry_sdk/_compat.py\\", line 56, in reraise\\n raise value\\n File \\"/usr/local/lib/python3.10/site-packages/sentry_sdk/integrations/celery.py\\", line 202, in _inner\\n return f(*args, **kwargs)\\n File \\"/usr/local/lib/python3.10/site-packages/celery/app/trace.py\\", line 734, in __protected_call__\\n return self.run(*args, **kwargs)\\n File \\"/usr/local/lib/python3.10/site-packages/celery/app/autoretry.py\\", line 54, in run\\n ret = task.retry(exc=exc, **retry_kwargs)\\n File \\"/usr/local/lib/python3.10/site-packages/celery/app/task.py\\", line 717, in retry\\n raise_with_context(exc)\\n File \\"/usr/local/lib/python3.10/site-packages/celery/app/autoretry.py\\", line 34, in run\\n return task._orig_run(*args, **kwargs)\\n File \\"/authentik/events/tasks.py\\", line 130, in notification_transport\\n raise exc\\n File \\"/authentik/events/tasks.py\\", line 126, in notification_transport\\n transport.send(notification)\\n File \\"/authentik/events/models.py\\", line 341, in send\\n return self.send_email(notification)\\n File \\"/authentik/events/models.py\\", line 463, in send_email\\n raise NotificationTransportError from exc\\nauthentik.events.models.NotificationTransportError: "} \N 2023-01-02 14:17:13.924879+00 {} 2024-01-02 14:17:13.924102+00 t {"pk": "3fe5e3c1df6f42e1a4a934178957c3b6", "app": "authentik_tenants", "name": "Tenant fallback", "model_name": "tenant"}
c1d32b85-3711-44c1-bd8b-918eba2599e2 system_task_exception authentik.events.monitored_tasks {"message": "Task notification_transport encountered an error: Traceback (most recent call last):\\n File \\"/usr/local/lib/python3.10/site-packages/celery/app/trace.py\\", line 451, in trace_task\\n R = retval = fun(*args, **kwargs)\\n File \\"/usr/local/lib/python3.10/site-packages/sentry_sdk/integrations/celery.py\\", line 207, in _inner\\n reraise(*exc_info)\\n File \\"/usr/local/lib/python3.10/site-packages/sentry_sdk/_compat.py\\", line 56, in reraise\\n raise value\\n File \\"/usr/local/lib/python3.10/site-packages/sentry_sdk/integrations/celery.py\\", line 202, in _inner\\n return f(*args, **kwargs)\\n File \\"/usr/local/lib/python3.10/site-packages/celery/app/trace.py\\", line 734, in __protected_call__\\n return self.run(*args, **kwargs)\\n File \\"/usr/local/lib/python3.10/site-packages/celery/app/autoretry.py\\", line 54, in run\\n ret = task.retry(exc=exc, **retry_kwargs)\\n File \\"/usr/local/lib/python3.10/site-packages/celery/app/task.py\\", line 717, in retry\\n raise_with_context(exc)\\n File \\"/usr/local/lib/python3.10/site-packages/celery/app/autoretry.py\\", line 34, in run\\n return task._orig_run(*args, **kwargs)\\n File \\"/authentik/events/tasks.py\\", line 130, in notification_transport\\n raise exc\\n File \\"/authentik/events/tasks.py\\", line 126, in notification_transport\\n transport.send(notification)\\n File \\"/authentik/events/models.py\\", line 341, in send\\n return self.send_email(notification)\\n File \\"/authentik/events/models.py\\", line 463, in send_email\\n raise NotificationTransportError from exc\\nauthentik.events.models.NotificationTransportError: "} \N 2023-01-02 14:17:14.593626+00 {} 2024-01-02 14:17:14.592229+00 t {"pk": "3fe5e3c1df6f42e1a4a934178957c3b6", "app": "authentik_tenants", "name": "Tenant fallback", "model_name": "tenant"}
521e9674-7fa5-4d70-b266-9b8048b640b0 model_created authentik.events.signals {"model": {"pk": 2, "app": "authentik_providers_saml", "name": "grafana-saml", "model_name": "samlprovider"}, "http_request": {"args": {}, "path": "/api/v3/providers/saml/", "method": "POST"}} 172.24.0.1 2023-01-02 14:20:39.41267+00 {"pk": 1, "email": "admin@localhost", "username": "akadmin"} 2024-01-02 14:20:39.41213+00 t {"pk": "1b45e4ae6c1b4ea89691df11c97baeef", "app": "authentik_tenants", "name": "Default tenant", "model_name": "tenant"}
8a5a4f65-cbd8-4617-a52a-515f7e008164 model_created authentik.events.signals {"model": {"pk": "506b118040694dda9405e185f355c34d", "app": "authentik_core", "name": "Grafana SAML", "model_name": "application"}, "http_request": {"args": {}, "path": "/api/v3/core/applications/", "method": "POST"}} 172.24.0.1 2023-01-02 14:21:07.723593+00 {"pk": 1, "email": "admin@localhost", "username": "akadmin"} 2024-01-02 14:21:07.722493+00 t {"pk": "1b45e4ae6c1b4ea89691df11c97baeef", "app": "authentik_tenants", "name": "Default tenant", "model_name": "tenant"}
7ee95403-f95f-4a94-be59-0eb1210a3b55 authorize_application authentik.providers.saml.views.flows {"flow": "a7ef54bb79594559979d9345c022e086", "http_request": {"args": {"query": ""}, "path": "/api/v3/flows/executor/default-provider-authorization-implicit-consent/", "method": "GET"}, "authorized_application": {"pk": "506b118040694dda9405e185f355c34d", "app": "authentik_core", "name": "Grafana SAML", "model_name": "application"}} 172.24.0.1 2023-01-02 14:21:13.310418+00 {"pk": 1, "email": "admin@localhost", "username": "akadmin"} 2024-01-02 14:21:13.30943+00 t {"pk": "1b45e4ae6c1b4ea89691df11c97baeef", "app": "authentik_tenants", "name": "Default tenant", "model_name": "tenant"}
2e67f976-24c8-4f39-9fb8-160c8df5fc48 authorize_application authentik.providers.saml.views.flows {"flow": "a7ef54bb79594559979d9345c022e086", "http_request": {"args": {"query": ""}, "path": "/api/v3/flows/executor/default-provider-authorization-implicit-consent/", "method": "GET"}, "authorized_application": {"pk": "506b118040694dda9405e185f355c34d", "app": "authentik_core", "name": "Grafana SAML", "model_name": "application"}} 172.24.0.1 2023-01-02 14:21:43.965265+00 {"pk": 1, "email": "admin@localhost", "username": "akadmin"} 2024-01-02 14:21:43.964033+00 t {"pk": "1b45e4ae6c1b4ea89691df11c97baeef", "app": "authentik_tenants", "name": "Default tenant", "model_name": "tenant"}
a2a95f65-9f92-496e-b577-55c844b90891 model_updated authentik.events.signals {"model": {"pk": 2, "app": "authentik_providers_saml", "name": "grafana-saml", "model_name": "samlprovider"}, "http_request": {"args": {}, "path": "/api/v3/providers/saml/2/", "method": "PUT"}} 172.24.0.1 2023-01-02 14:22:28.185263+00 {"pk": 1, "email": "admin@localhost", "username": "akadmin"} 2024-01-02 14:22:28.184795+00 t {"pk": "1b45e4ae6c1b4ea89691df11c97baeef", "app": "authentik_tenants", "name": "Default tenant", "model_name": "tenant"}
95797e9f-e818-4f5d-9fdb-2d499833ae4c model_updated authentik.events.signals {"model": {"pk": 2, "app": "authentik_providers_saml", "name": "grafana-saml", "model_name": "samlprovider"}, "http_request": {"args": {}, "path": "/api/v3/providers/saml/2/", "method": "PUT"}} 172.24.0.1 2023-01-02 14:23:24.663957+00 {"pk": 1, "email": "admin@localhost", "username": "akadmin"} 2024-01-02 14:23:24.663405+00 t {"pk": "1b45e4ae6c1b4ea89691df11c97baeef", "app": "authentik_tenants", "name": "Default tenant", "model_name": "tenant"}
aa49c5e8-b508-4046-907e-cfdca67edf75 configuration_error authentik.providers.saml.views.sso {"message": "Failed to verify signature", "provider": {"pk": 2, "app": "authentik_providers_saml", "name": "grafana-saml", "model_name": "samlprovider"}} \N 2023-01-02 16:20:27.103437+00 {} 2024-01-02 16:20:27.102874+00 t {"pk": "69cc7a86348f4144973b0cbaaea44e3f", "app": "authentik_tenants", "name": "Tenant fallback", "model_name": "tenant"}
9aac6d1c-02e9-4e51-b705-85a86f1919df system_task_exception authentik.events.monitored_tasks {"message": "Task notification_transport encountered an error: Traceback (most recent call last):\\n File \\"/usr/local/lib/python3.10/site-packages/celery/app/trace.py\\", line 451, in trace_task\\n R = retval = fun(*args, **kwargs)\\n File \\"/usr/local/lib/python3.10/site-packages/sentry_sdk/integrations/celery.py\\", line 207, in _inner\\n reraise(*exc_info)\\n File \\"/usr/local/lib/python3.10/site-packages/sentry_sdk/_compat.py\\", line 56, in reraise\\n raise value\\n File \\"/usr/local/lib/python3.10/site-packages/sentry_sdk/integrations/celery.py\\", line 202, in _inner\\n return f(*args, **kwargs)\\n File \\"/usr/local/lib/python3.10/site-packages/celery/app/trace.py\\", line 734, in __protected_call__\\n return self.run(*args, **kwargs)\\n File \\"/usr/local/lib/python3.10/site-packages/celery/app/autoretry.py\\", line 54, in run\\n ret = task.retry(exc=exc, **retry_kwargs)\\n File \\"/usr/local/lib/python3.10/site-packages/celery/app/task.py\\", line 717, in retry\\n raise_with_context(exc)\\n File \\"/usr/local/lib/python3.10/site-packages/celery/app/autoretry.py\\", line 34, in run\\n return task._orig_run(*args, **kwargs)\\n File \\"/authentik/events/tasks.py\\", line 130, in notification_transport\\n raise exc\\n File \\"/authentik/events/tasks.py\\", line 126, in notification_transport\\n transport.send(notification)\\n File \\"/authentik/events/models.py\\", line 341, in send\\n return self.send_email(notification)\\n File \\"/authentik/events/models.py\\", line 463, in send_email\\n raise NotificationTransportError from exc\\nauthentik.events.models.NotificationTransportError: "} \N 2023-01-02 16:20:32.525925+00 {} 2024-01-02 16:20:32.524994+00 t {"pk": "3fe5e3c1df6f42e1a4a934178957c3b6", "app": "authentik_tenants", "name": "Tenant fallback", "model_name": "tenant"}
d93d9f43-d318-4f57-8fc5-58859aafd0a6 authorize_application authentik.providers.saml.views.flows {"flow": "a7ef54bb79594559979d9345c022e086", "http_request": {"args": {"query": ""}, "path": "/api/v3/flows/executor/default-provider-authorization-implicit-consent/", "method": "GET"}, "authorized_application": {"pk": "506b118040694dda9405e185f355c34d", "app": "authentik_core", "name": "Grafana SAML", "model_name": "application"}} 172.24.0.1 2023-01-02 16:20:39.35962+00 {"pk": 1, "email": "admin@localhost", "username": "akadmin"} 2024-01-02 16:20:39.358773+00 t {"pk": "1b45e4ae6c1b4ea89691df11c97baeef", "app": "authentik_tenants", "name": "Default tenant", "model_name": "tenant"}
9a1859e1-089a-46c9-a2a9-5ca91e1a14da model_updated authentik.events.signals {"model": {"pk": 2, "app": "authentik_providers_saml", "name": "grafana-saml", "model_name": "samlprovider"}, "http_request": {"args": {}, "path": "/api/v3/providers/saml/2/", "method": "PUT"}} 172.24.0.1 2023-01-02 16:21:00.718418+00 {"pk": 1, "email": "admin@localhost", "username": "akadmin"} 2024-01-02 16:21:00.717578+00 t {"pk": "1b45e4ae6c1b4ea89691df11c97baeef", "app": "authentik_tenants", "name": "Default tenant", "model_name": "tenant"}
58277880-d969-4e13-ba98-dfca29dc2815 model_updated authentik.events.signals {"model": {"pk": 2, "app": "authentik_providers_saml", "name": "grafana-saml", "model_name": "samlprovider"}, "http_request": {"args": {}, "path": "/api/v3/providers/saml/2/", "method": "PUT"}} 172.24.0.1 2023-01-02 16:21:20.595211+00 {"pk": 1, "email": "admin@localhost", "username": "akadmin"} 2024-01-02 16:21:20.594713+00 t {"pk": "1b45e4ae6c1b4ea89691df11c97baeef", "app": "authentik_tenants", "name": "Default tenant", "model_name": "tenant"}
9c9fc310-2115-4aee-a0f0-d91abc0dae03 authorize_application authentik.providers.saml.views.flows {"flow": "a7ef54bb79594559979d9345c022e086", "http_request": {"args": {"query": ""}, "path": "/api/v3/flows/executor/default-provider-authorization-implicit-consent/", "method": "GET"}, "authorized_application": {"pk": "506b118040694dda9405e185f355c34d", "app": "authentik_core", "name": "Grafana SAML", "model_name": "application"}} 172.24.0.1 2023-01-02 16:21:26.679637+00 {"pk": 1, "email": "admin@localhost", "username": "akadmin"} 2024-01-02 16:21:26.67829+00 t {"pk": "1b45e4ae6c1b4ea89691df11c97baeef", "app": "authentik_tenants", "name": "Default tenant", "model_name": "tenant"}
2f1ac464-0e79-47ea-b3df-bf70f90d22c7 authorize_application authentik.providers.saml.views.flows {"flow": "a7ef54bb79594559979d9345c022e086", "http_request": {"args": {"query": ""}, "path": "/api/v3/flows/executor/default-provider-authorization-implicit-consent/", "method": "GET"}, "authorized_application": {"pk": "506b118040694dda9405e185f355c34d", "app": "authentik_core", "name": "Grafana SAML", "model_name": "application"}} 172.24.0.1 2023-01-02 16:21:47.199639+00 {"pk": 1, "email": "admin@localhost", "username": "akadmin"} 2024-01-02 16:21:47.198789+00 t {"pk": "1b45e4ae6c1b4ea89691df11c97baeef", "app": "authentik_tenants", "name": "Default tenant", "model_name": "tenant"}
d4549f57-3afb-4e6f-8726-be968bbc902f model_updated authentik.events.signals {"model": {"pk": 2, "app": "authentik_providers_saml", "name": "grafana-saml", "model_name": "samlprovider"}, "http_request": {"args": {}, "path": "/api/v3/providers/saml/2/", "method": "PUT"}} 172.24.0.1 2023-01-02 16:22:24.896791+00 {"pk": 1, "email": "admin@localhost", "username": "akadmin"} 2024-01-02 16:22:24.895837+00 t {"pk": "1b45e4ae6c1b4ea89691df11c97baeef", "app": "authentik_tenants", "name": "Default tenant", "model_name": "tenant"}
8d6e7456-f3f6-4806-a1ae-286cd50c6e54 authorize_application authentik.providers.saml.views.flows {"flow": "a7ef54bb79594559979d9345c022e086", "http_request": {"args": {"query": ""}, "path": "/api/v3/flows/executor/default-provider-authorization-implicit-consent/", "method": "GET"}, "authorized_application": {"pk": "506b118040694dda9405e185f355c34d", "app": "authentik_core", "name": "Grafana SAML", "model_name": "application"}} 172.24.0.1 2023-01-02 16:22:37.062381+00 {"pk": 1, "email": "admin@localhost", "username": "akadmin"} 2024-01-02 16:22:37.061613+00 t {"pk": "1b45e4ae6c1b4ea89691df11c97baeef", "app": "authentik_tenants", "name": "Default tenant", "model_name": "tenant"}
7203b3e8-4475-498f-b837-5775a0501e86 authorize_application authentik.providers.saml.views.flows {"flow": "a7ef54bb79594559979d9345c022e086", "http_request": {"args": {"query": ""}, "path": "/api/v3/flows/executor/default-provider-authorization-implicit-consent/", "method": "GET"}, "authorized_application": {"pk": "506b118040694dda9405e185f355c34d", "app": "authentik_core", "name": "Grafana SAML", "model_name": "application"}} 172.24.0.1 2023-01-02 16:26:08.685203+00 {"pk": 1, "email": "admin@localhost", "username": "akadmin"} 2024-01-02 16:26:08.684412+00 t {"pk": "1b45e4ae6c1b4ea89691df11c97baeef", "app": "authentik_tenants", "name": "Default tenant", "model_name": "tenant"}
7384e1a4-157e-47dd-ad30-f0c4030a1b54 authorize_application authentik.providers.saml.views.flows {"flow": "a7ef54bb79594559979d9345c022e086", "http_request": {"args": {"query": "SAMLRequest=nJJBj9MwEIX%2FiuV76sRRmtbaRCpbISotUG0LB25TZ7q15NjBMwH236Nmi1QklANH2%2FPe53kzDwS9H8xm5Et4xu8jEotfvQ9krg%2BNHFMwEciRCdAjGbbmsPn4ZPQiN0CEiV0M8k4yzGuGFDna6KXYbRvpumxZl%2BeyrivbrWoN5akDrOtTpYuyWq3XS1utKnvSxVmKr5jIxdBIvcil2BGNuAvEELiROtdllhdZro%2FF0uiVKcpFWa%2B%2FSbFFYheAJ%2BWFeTBK%2BWjBXyKxWed5rmAYvLNTibq2oF4SnCFANh2Iojq50LnwohJ2LqFlJcXmT%2FOPMdDYYzpg%2BuEsfnl%2B%2BgenvHImO7Akxf6Wwrs33%2FnIbnAyH47Hfbb%2FfDjKdhqbmTJI4n1MPfC8yfXGddl5KjUY2PGrbGf%2B2SNDBwwP6g7V3tblE%2FS42%2B6jd%2Fb1P%2FCcIJDDwFJsvI8%2FHxMCYyM5jShV%2B4b8eynb3wEAAP%2F%2F&RelayState=MVFtczE2eEVENFo0amhyUEtoUFkySDJ5Wm5CRkRQWXJYaW42eTBMNTIyYkZNRENhS0Z8WkV6SHRydFZ6&SigAlg=http%3A%2F%2Fwww.w3.org%2F2001%2F04%2Fxmldsig-more%23rsa-sha256&Signature=Uds21tE5ZR3SmvxFqy9VrYyL8ujqShawaGcz0V77dayLPpeykS6JN36ZoTD4eHSPUqmHqvTziWpLC8O2IlbfANmm8%2F58AOWsxSaKjdqwlKgWeLSQUrqhDXLOdOfabzMXPFkcBg9jS7P%2FXRl4rGSyjS%2BCYTJdazdrM8w5JOBvO%2B%2B2NGfxbo1hcHbGprQTt7ScDsxt7WPcEE0GRVhEbOZAym%2Fp6ZwCGB%2FkxC%2BGN68hdbXKD%2B51X0SIdF8Apy%2BYw4nshrIqteXb6jcoTwfNQdNY25wnuJ3J4LXaE5ZqajjjIIfPFYjBsnRFuzFUed3372rahiX5ax0SWp7Fj3uxyG6%2FQil9MYSvCX1Kw0rQf5zKP%2FFohivJ3vtbgHsVa6mqgWPR%2BFyiX5xZbDi4DaHGof%2F7NM5f9g3CgcaQr5v8%2BK8eHn%2Fl1EjDlJSDr2BSJ3NX68581527VAcWZ2OjXRIeodh5DKBrQ9F9%2BTW4AEuTlRBPUWdL15209HG6MDcN5p6M0OgYh%2B6STW5FY0aqgSFBjxjXAGk%2BlKQTq%2BrpJnLQXxgJlby3%2BYkyO92Ick34NsxoaDr5ySYYtM6M%2FuZP9443t8B9xun407wdaja3CNWP8iXuM2%2FhS1JCDud%2Fb8oCq14l0sITUqUK4R%2BCEd7odJDUuSNdDjHiM%2BEW1mf3MyuB68M1dhE%3D"}, "path": "/api/v3/flows/executor/default-provider-authorization-implicit-consent/", "method": "GET"}, "authorized_application": {"pk": "506b118040694dda9405e185f355c34d", "app": "authentik_core", "name": "Grafana SAML", "model_name": "application"}} 172.24.0.1 2023-01-02 16:28:13.791097+00 {"pk": 1, "email": "admin@localhost", "username": "akadmin"} 2024-01-02 16:28:13.789745+00 t {"pk": "1b45e4ae6c1b4ea89691df11c97baeef", "app": "authentik_tenants", "name": "Default tenant", "model_name": "tenant"}
b4315108-6c82-4ff3-aeea-56a31d071961 model_updated authentik.events.signals {"model": {"pk": 2, "app": "authentik_providers_saml", "name": "grafana-saml", "model_name": "samlprovider"}, "http_request": {"args": {}, "path": "/api/v3/providers/saml/2/", "method": "PUT"}} 172.24.0.1 2023-01-02 16:30:58.798901+00 {"pk": 1, "email": "admin@localhost", "username": "akadmin"} 2024-01-02 16:30:58.797248+00 t {"pk": "1b45e4ae6c1b4ea89691df11c97baeef", "app": "authentik_tenants", "name": "Default tenant", "model_name": "tenant"}
439439e2-e8e7-4af6-b3ef-e563b36f674a model_created authentik.events.signals {"model": {"pk": "ca14281ce0254691b5a23ee415920e80", "app": "authentik_core", "name": "extra-group", "model_name": "group"}, "http_request": {"args": {}, "path": "/api/v3/core/groups/", "method": "POST"}} 172.24.0.1 2023-01-02 16:33:50.270344+00 {"pk": 1, "email": "admin@localhost", "username": "akadmin"} 2024-01-02 16:33:50.269707+00 t {"pk": "1b45e4ae6c1b4ea89691df11c97baeef", "app": "authentik_tenants", "name": "Default tenant", "model_name": "tenant"}
c5efd86a-6999-4a7b-8fd9-305b26d96bc6 login authentik.events.signals {"auth_method": "password", "http_request": {"args": {"query": "next=%2Fapplication%2Fsaml%2Fgrafana-saml%2Fsso%2Fbinding%2Fredirect%2F%3FSAMLRequest%3DnJJBb9swDIX%252FiqC7I1t2U0%252BoDWQNhgXotqDJdtiNlelGgCx5Ir2t%252F36ImwEZMPjQoyS%252B94mPvCMY%252FGg2E5%252FCI%252F6YkFj8Hnwgc35o5JSCiUCOTIABybA1h82nB6NXuQEiTOxikFeScVkzpsjRRi%252FFbttI12VY6luLsF5bjbaobxHqQvc3T1VVV7qsir4rboq%252BRim%252BYSIXQyP1KpdiRzThLhBD4EbqXJdZXmS5PhZrU9am0Kt19V2KLRK7ADwLT8yjUcpHC%252F4Uic27PM8VjKN3di5R5w7Uc4IeAmTzgSiqJxc6F55Vws4ltKyk2Pzt%252FT4GmgZMB0w%252FncWvjw%252F%252F4ZRnzmwHlqTYX0J4%252F%252Bq7nNgFTubj8bjP9l8OR9nOUzNzBEl8iGkAXjY537gu6%252BdSg4Edv8h24Z8DMnTAcKeuUO1lWz7DgLvtPnpnX96A5wSBHAaWYuN9%252FHWfEBgbyWlCqdpX5L872f4JAAD%252F%252Fw%253D%253D%26RelayState%3DR3cwMXZJMU9xZUVaY1RZVmxVMDkwVk1DSEVkQzYzR2hBU3NNUHNkVlZxSEt1SDdrUDJ8Z2pPbHA5cFZ6%26SigAlg%3Dhttp%253A%252F%252Fwww.w3.org%252F2001%252F04%252Fxmldsig-more%2523rsa-sha256%26Signature%3DBX5F2GCFaeadEWb17AxG2mVisyVfBGrNRczpBwqe0TwTQZKySlVpaED8Vqak2hSyOC9HdKSkRmYGBJqrKPImbGzdeptgxXqlr8usnoBmxy1UF8GKrOM2ptPOqMsXargtigpS7mLatQN1spTkxyDUnfQfCknvH%252BgjQymkGDjtMtOQLEP2y3webPtuBiwv43Wl4YzojxSXALALITtANMgCqdsE8omJ%252FtuZZG2iu31j%252BJk3OTDWhZn6QBNNrV%252FMEPZOVD5CpnxagNhPC4J74rmWP2JA6Yu%252BcqFaCBTlty0do7D%252FQVvdzjHgrnAaosxltAtUkDWS47%252Fo1Ed9Jp2jTzsLzOh2%252F2EXZB7UrPIiQL28THjChuMv9ohoVrwHi6Z77SSJzJJYQmUKPC6IUg4PeoUY%252FpghlrtqxTDeghePUcLuMTusLu4wf%252BdMEvhwi20Eaz6ABr7F0TYAxnKfdEwsG%252FEIkBHa109%252Fv3YqDKKbKUuJKyUeu%252F%252Bo9YwbZG5eRDqKoBoNRbAyDOBVqXMgkTNUEhs5qtLDbOlgcim%252F5Tum5usf921zEn0Z01vGTO2bjrjdRbdJiAfAPhOj%252F69n7rJn%252FklU8NtXdiNHYZn0MLAdOKpBChjnfjehCp08n%252BG4ZcCXMe62gnD2Art0HJf30z2N76vJsoyrXMypc%252BN6WpUQSpsgb6w%253D"}, "path": "/api/v3/flows/executor/default-authentication-flow/", "method": "GET"}, "auth_method_args": {}} 172.24.0.1 2023-01-02 16:38:22.087813+00 {"pk": 5, "email": "authentik-admin@localhost", "username": "authentik-admin"} 2024-01-02 16:38:22.087203+00 t {"pk": "1b45e4ae6c1b4ea89691df11c97baeef", "app": "authentik_tenants", "name": "Default tenant", "model_name": "tenant"}
0ffd3d0e-31f3-4b55-bdf7-d00160afcfdc authorize_application authentik.providers.saml.views.flows {"flow": "a7ef54bb79594559979d9345c022e086", "http_request": {"args": {"query": "SAMLRequest=nJJBb9swDIX%2FiqC7I1t2U0%2BoDWQNhgXotqDJdtiNlelGgCx5Ir2t%2F36ImwEZMPjQoyS%2B94mPvCMY%2FGg2E5%2FCI%2F6YkFj8Hnwgc35o5JSCiUCOTIABybA1h82nB6NXuQEiTOxikFeScVkzpsjRRi%2FFbttI12VY6luLsF5bjbaobxHqQvc3T1VVV7qsir4rboq%2BRim%2BYSIXQyP1KpdiRzThLhBD4EbqXJdZXmS5PhZrU9am0Kt19V2KLRK7ADwLT8yjUcpHC%2F4Uic27PM8VjKN3di5R5w7Uc4IeAmTzgSiqJxc6F55Vws4ltKyk2Pzt%2FT4GmgZMB0w%2FncWvjw%2F%2F4ZRnzmwHlqTYX0J4%2F%2Bq7nNgFTubj8bjP9l8OR9nOUzNzBEl8iGkAXjY537gu6%2BdSg4Edv8h24Z8DMnTAcKeuUO1lWz7DgLvtPnpnX96A5wSBHAaWYuN9%2FHWfEBgbyWlCqdpX5L872f4JAAD%2F%2Fw%3D%3D&RelayState=R3cwMXZJMU9xZUVaY1RZVmxVMDkwVk1DSEVkQzYzR2hBU3NNUHNkVlZxSEt1SDdrUDJ8Z2pPbHA5cFZ6&SigAlg=http%3A%2F%2Fwww.w3.org%2F2001%2F04%2Fxmldsig-more%23rsa-sha256&Signature=BX5F2GCFaeadEWb17AxG2mVisyVfBGrNRczpBwqe0TwTQZKySlVpaED8Vqak2hSyOC9HdKSkRmYGBJqrKPImbGzdeptgxXqlr8usnoBmxy1UF8GKrOM2ptPOqMsXargtigpS7mLatQN1spTkxyDUnfQfCknvH%2BgjQymkGDjtMtOQLEP2y3webPtuBiwv43Wl4YzojxSXALALITtANMgCqdsE8omJ%2FtuZZG2iu31j%2BJk3OTDWhZn6QBNNrV%2FMEPZOVD5CpnxagNhPC4J74rmWP2JA6Yu%2BcqFaCBTlty0do7D%2FQVvdzjHgrnAaosxltAtUkDWS47%2Fo1Ed9Jp2jTzsLzOh2%2F2EXZB7UrPIiQL28THjChuMv9ohoVrwHi6Z77SSJzJJYQmUKPC6IUg4PeoUY%2FpghlrtqxTDeghePUcLuMTusLu4wf%2BdMEvhwi20Eaz6ABr7F0TYAxnKfdEwsG%2FEIkBHa109%2Fv3YqDKKbKUuJKyUeu%2F%2Bo9YwbZG5eRDqKoBoNRbAyDOBVqXMgkTNUEhs5qtLDbOlgcim%2F5Tum5usf921zEn0Z01vGTO2bjrjdRbdJiAfAPhOj%2F69n7rJn%2FklU8NtXdiNHYZn0MLAdOKpBChjnfjehCp08n%2BG4ZcCXMe62gnD2Art0HJf30z2N76vJsoyrXMypc%2BN6WpUQSpsgb6w%3D"}, "path": "/api/v3/flows/executor/default-provider-authorization-implicit-consent/", "method": "GET"}, "authorized_application": {"pk": "506b118040694dda9405e185f355c34d", "app": "authentik_core", "name": "Grafana SAML", "model_name": "application"}} 172.24.0.1 2023-01-02 16:38:22.508665+00 {"pk": 5, "email": "authentik-admin@localhost", "username": "authentik-admin"} 2024-01-02 16:38:22.508022+00 t {"pk": "1b45e4ae6c1b4ea89691df11c97baeef", "app": "authentik_tenants", "name": "Default tenant", "model_name": "tenant"}
025e97e0-ee2c-4178-a410-2b832340a35a authorize_application authentik.providers.saml.views.flows {"flow": "a7ef54bb79594559979d9345c022e086", "http_request": {"args": {"query": ""}, "path": "/api/v3/flows/executor/default-provider-authorization-implicit-consent/", "method": "GET"}, "authorized_application": {"pk": "506b118040694dda9405e185f355c34d", "app": "authentik_core", "name": "Grafana SAML", "model_name": "application"}} 172.24.0.1 2023-01-02 16:40:38.094961+00 {"pk": 1, "email": "admin@localhost", "username": "akadmin"} 2024-01-02 16:40:38.094424+00 t {"pk": "1b45e4ae6c1b4ea89691df11c97baeef", "app": "authentik_tenants", "name": "Default tenant", "model_name": "tenant"}
8cdc9b41-47bb-440b-9ce8-3fa9d7c81800 authorize_application authentik.providers.saml.views.flows {"flow": "a7ef54bb79594559979d9345c022e086", "http_request": {"args": {"query": "SAMLRequest=nJJBj9MwEIX%2FSuR7ajvpJsTaRCpbISotUG0LB25TZ7q15NjBMwH236Nmi1Qk1MMebc97n%2BfN3BMMfjSriU%2FhCX9MSJz9Hnwgc35oxZSCiUCOTIABybA1u9WnR1MslAEiTOxiEFeS8bZmTJGjjV5km3UrXJ%2Frpa5RN1Dp4nBodF%2FZEnVT9XWpbH1X3tW2Pmo8vBPZN0zkYmhFsVAi2xBNuAnEELgVhSrKXOlcFXtdmWVl1HJRNvq7yNZI7ALwrDwxj0ZKHy34UyQ2jVJKwjh6Z%2BcSeW5BPic4QoB8PhBFeXChd%2BFZJuxdQstSZKu%2FzT%2FEQNOAaYfpp7P49enxP5zyzJntwJLItpcU3r%2F63o7sAifzcb%2Ff5tsvu73o5rGZOYOUfYhpAL5tcr5xfX6cSw0Gdvwiuhv%2FHJChB4Z7eYXqLuvyGQbcrLfRO%2FvyBjwnCOQwsMhW3sdfDwmBsRWcJhSye0X%2Bu5TdnwAAAP%2F%2F&RelayState=VVk1bmU0UDk4clBnUnNOVk5PNFBzV3hQamFHUVgxdEszS094WUN3SEZtWnlLczg0VXV8U2F5SGhydFZ6&SigAlg=http%3A%2F%2Fwww.w3.org%2F2001%2F04%2Fxmldsig-more%23rsa-sha256&Signature=mW4CQva3Vem85Dq5yo%2BBclqHaloTreonGr4VCel4SvbiRfF5jgQDGEcxn7kcxDw8JZym8iquo7Ivx%2BM%2FuciSTWOMp2n%2FTzbkkR7QxSfYjBLUXTsoh0JBZvh2fVsJMr10rYaW%2Frig8mGmP697eEhRyma9t5HJnu6mkvE%2Biszr3qQmv85nX%2B9Ac6IBOQOsiSheeKqdZm3Yj3gb3IGZjJ5Y8xmGZSmQ%2BLbiB7AerHM8KNUqysRBoyDBcG5YTtTNao4ImNBkCxh0a45Geur3y0LwEmt0lQkY2YJzW4Uh%2B3dKCK2yRqnTZfEK7nvH37Ue0uc1Fx9zH6hjgijSqWfYAYEJWf%2B4sM%2BhMZ8tHV93bLzzaPXH%2FfhAYyo%2FMREW2AKlIYElK6dQA68wblp%2FxYXm%2BXgJoYs%2FHQqPrQLU%2BE2Bd5Me5xvUIfD2KBR3Lx9GsY8%2F0n21HTajAgxga3NuscTGlP9Is%2FLdOxfu9IY3d77ecSTTUWgkcR%2BRyEo1ApUGLWZrAp5WK1IWMbfBf0hhPmQCccjgjN%2B8kDgAE9ETst36804uotZH6VY4iuTJhdnX4EPcFl848E6BwnrnlP%2FyJ16Qb7nQGWn2zGVGFTncEtwH%2FaUb1LKp6yTuMSuSQRcqzNyFuvJDBvmWiGTbGzlt9VCbwtIVTInSI3NLin%2BMErSktt5AFVg%3D"}, "path": "/api/v3/flows/executor/default-provider-authorization-implicit-consent/", "method": "GET"}, "authorized_application": {"pk": "506b118040694dda9405e185f355c34d", "app": "authentik_core", "name": "Grafana SAML", "model_name": "application"}} 172.24.0.1 2023-01-02 16:46:04.805431+00 {"pk": 1, "email": "admin@localhost", "username": "akadmin"} 2024-01-02 16:46:04.80479+00 t {"pk": "1b45e4ae6c1b4ea89691df11c97baeef", "app": "authentik_tenants", "name": "Default tenant", "model_name": "tenant"}
\.
@@ -2752,6 +2788,9 @@ a3998ed2-5beb-4a26-b395-f5b579d632ca model_updated authentik.events.signals {"mo
--
COPY public.authentik_events_notification (uuid, severity, body, created, seen, event_id, user_id) FROM stdin;
2e407e51-2f8e-4386-8f0e-44075f108506 alert Changelog: https://goauthentik.io/docs/releases/2022.12#fixed-in-2022121 2023-01-02 14:17:10.978208+00 f eeb1be95-4ade-4812-93b0-2c87db29716c 1
b60edc8e-400f-4598-8985-b16194cd6fe1 alert Changelog: https://goauthentik.io/docs/releases/2022.12#fixed-in-2022121 2023-01-02 14:17:11.648064+00 f 12d75a37-20ee-44f4-9f1b-9f99d6dfdeea 1
b124e245-e4fc-457d-9171-329220ca358f alert Failed to verify signature 2023-01-02 16:20:28.230329+00 f aa49c5e8-b508-4046-907e-cfdca67edf75 1
\.
@@ -3065,10 +3104,11 @@ cba52268-0d9a-40bd-ae7f-b723f5e05671 any
417e795d-6832-40b6-b083-4462357471cb any
0a3c97a9-ac46-45cb-8718-a2984fff154a any
081ce9e5-201f-48e9-af60-7c10076f0857 any
0e32dc99-655c-42f4-aca4-f3f271e010fd any
105415a2-7753-4a7f-bdf5-ed9841420289 any
e4c83e8e-69f4-4993-a0c2-f99552fd76d0 any
9fdfd009-537e-4b36-8cc8-98f62620f410 any
0e32dc99-655c-42f4-aca4-f3f271e010fd any
506b1180-4069-4dda-9405-e185f355c34d any
\.
@@ -3082,6 +3122,8 @@ COPY public.authentik_policies_reputation_reputation (reputation_uuid, identifie
d36ccca8-c986-4517-8110-761eac3d8be0 akadmin 192.168.48.1 {} -2 2022-10-25 09:17:04.253267+00
f433dfcb-3aa4-4d93-8abc-6fe2c4a77f77 ldapservice 192.168.48.1 {} 5 2022-10-25 09:53:31.155505+00
2f00817d-e5f1-4e9d-bc4e-8210b2ca2258 authentik-admin 192.168.48.1 {} 1 2022-10-25 09:54:01.492491+00
85999073-85b3-4f12-93b8-a98b770daa41 akadmin 172.24.0.1 {} 1 2023-01-02 14:16:56.149781+00
0eb5ae80-8ca7-4431-bac6-2032e2e5ef97 authentik-admin 172.24.0.1 {} 1 2023-01-02 16:38:23.763498+00
\.
@@ -3182,6 +3224,7 @@ c5d93ce6-8cda-42fa-9c19-3837ba67788a http://schemas.xmlsoap.org/claims/Group \N
--
COPY public.authentik_providers_saml_samlprovider (provider_ptr_id, acs_url, audience, issuer, assertion_valid_not_before, assertion_valid_not_on_or_after, session_valid_not_on_or_after, digest_algorithm, signature_algorithm, signing_kp_id, sp_binding, verification_kp_id, name_id_mapping_id) FROM stdin;
2 http://localhost:3000/saml/acs http://localhost:3000/saml/metadata minutes=-5 minutes=5 minutes=86400 http://www.w3.org/2001/04/xmlenc#sha256 http://www.w3.org/2001/04/xmldsig-more#rsa-sha256 6304c995-9003-4fc4-9ba2-3d32016fb04e post \N 38ac59c3-e754-44e7-9790-5108a2cd027c
\.
@@ -4053,9 +4096,9 @@ COPY public.guardian_groupobjectpermission (id, object_pk, content_type_id, grou
--
COPY public.guardian_userobjectpermission (id, object_pk, content_type_id, permission_id, user_id) FROM stdin;
2 0c24aadc-f97e-4720-a70f-72a190b0cafc 15 63 3
20 efe635b9-2ce7-4de4-977f-9f25b9f36d97 15 63 4
21 1 30 125 4
22 efe635b9-2ce7-4de4-977f-9f25b9f36d97 15 63 4
23 1 30 125 4
24 0c24aadc-f97e-4720-a70f-72a190b0cafc 15 63 3
\.
@@ -4108,14 +4151,14 @@ SELECT pg_catalog.setval('public.auth_permission_id_seq', 356, true);
-- Name: authentik_core_provider_id_seq; Type: SEQUENCE SET; Schema: public; Owner: authentik
--
SELECT pg_catalog.setval('public.authentik_core_provider_id_seq', 1, true);
SELECT pg_catalog.setval('public.authentik_core_provider_id_seq', 2, true);
--
-- Name: authentik_core_provider_property_mappings_id_seq; Type: SEQUENCE SET; Schema: public; Owner: authentik
--
SELECT pg_catalog.setval('public.authentik_core_provider_property_mappings_id_seq', 1, false);
SELECT pg_catalog.setval('public.authentik_core_provider_property_mappings_id_seq', 7, true);
--
@@ -4143,14 +4186,14 @@ SELECT pg_catalog.setval('public.authentik_core_user_id_seq', 8, true);
-- Name: authentik_core_user_pb_groups_id_seq; Type: SEQUENCE SET; Schema: public; Owner: authentik
--
SELECT pg_catalog.setval('public.authentik_core_user_pb_groups_id_seq', 5, true);
SELECT pg_catalog.setval('public.authentik_core_user_pb_groups_id_seq', 7, true);
--
-- Name: authentik_core_user_user_permissions_id_seq; Type: SEQUENCE SET; Schema: public; Owner: authentik
--
SELECT pg_catalog.setval('public.authentik_core_user_user_permissions_id_seq', 30, true);
SELECT pg_catalog.setval('public.authentik_core_user_user_permissions_id_seq', 34, true);
--
@@ -4290,7 +4333,7 @@ SELECT pg_catalog.setval('public.guardian_groupobjectpermission_id_seq', 1, fals
-- Name: guardian_userobjectpermission_id_seq; Type: SEQUENCE SET; Schema: public; Owner: authentik
--
SELECT pg_catalog.setval('public.guardian_userobjectpermission_id_seq', 21, true);
SELECT pg_catalog.setval('public.guardian_userobjectpermission_id_seq', 24, true);
--
@@ -39,7 +39,7 @@
- "authentik:authentik"
authentik:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2022.10.0}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2022.12.1}
restart: unless-stopped
container_name: authentik
command: server
@@ -66,7 +66,7 @@
- "authentikredis:authentikredis"
authentik-worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2022.10.0}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2022.12.1}
restart: unless-stopped
container_name: authentik-worker
command: worker
@@ -88,4 +88,4 @@
- ./geoip:/geoip
links:
- "authentikdb:authentikdb"
- "authentikredis:authentikredis"
- "authentikredis:authentikredis"
@@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
MIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQDOQQHtZEMgy/WJ
f7gqYMko8UIImFkr5Q1QsIQWalX/34jS8d2Pnl5WPZ+7FqG56ZZ0L4aiBcv7JsA+
Eu8nCeXk6WiMf25yjsi5Aj8PwLXgjAr+q28Gi6EKRvCX/6zcYYOrLt2YWTc706aW
VIAOUikVFKKgRzqX/yyyF96WzJde/7mQq7zsJFgNMt0L8yHFrl0DXOpRme++t965
zjVnwCPeQU1Jd5FswoD56ObSQD4Fz4xdeE/hPiAG6TWBHQGjTLawQNvSaUnUf8Ua
QlnqfXaCPcgFgx+t8nCPTQ9VS9cCr/yu1+J4qBm9ytvBHt8nu6lpZE67GaSeMYk+
HXUfObh3rnczfgGQM1gyv97j0qS26/u8Mbwo4fHt/Xqvdl7wIvU1jLgNeJyimVmj
+l3I9git23Vs8xnh/Cq5+A85FVmbf/PYkOl4djYO2o/XiMUNbl9jN0nhm3xhmC0D
O4FtkHhAwWg4r70aR9kzP/wu0+ugQq12EQOSzKAhs+bJ+7Iju0li0nhjbxhvDacT
xcndJHXPKG+MJOBCjTrfEGA+IANUPVVy+me3efX/lTCC0tInNUu7xrP4ePiKrtGN
5h2bAYUd5xxFm/+7x02aOoyS31REZydymzyzik3dUl6pBfj8bqfbgKgHLc3O7awa
BKcbpQ6RUZz1ahuM9GsrN2mfGb56twIDAQABAoICAQC30wSOua+CiYWbOLPBQWXI
Ec5SrsW683h8k/YFrFxdorLlnx9jIzjdECtOqYsWkXPAtaL+wL/eZdks+d8gIhg9
fzHpFqIvg8at7ayas5zmNqqN8+kOImAzMqq4Le24tUiOx0RExi8OKULt9P30xZFL
8opG43T7hzgkulVufYXj63NN12KX7IR1rwGH5vgQoajQmH1WoSYmCuZ9H6ZFg9/z
9KuXcPOxxWECW2tttEgTEbn+jH5VN/N/dVZ0bo/JJVQlnojM7X4Z4I6hKsXM0mlP
BxVApG8Vr2BWt82UrfVBtew1QL/d3aSjnSti4cruNLl27ymkY+RuAaRaMhLy85TN
q3vWXoVA0vJiG3mk3vr9pzO3tkbSS4i/xcnRkl62YMY59bZ3YZlGcSqOj+ohcZNs
9IMtn3ivqqSYWCysuyOa5vIhoa/ZmFHVavudGlfQ9CRS08G5u2KNkjyUAlFXwEgy
dXg4FYNhqcZ9vDZ5/kwhgpoP1et/SS+BInM/1sXX23wyRtaq6lEkMmZZ0TWvdkkp
16kDWTfbrol8WTKLB5GAs0tV4sTIe56iH2tuNA8qmXer004R3Ye+9qzIu7tsztlJ
XzX9xwqKGouSuXrVkLTgrwNBLqJUsf7YcPoCTsF7GsucOkpPCxmEPoYT4IzY/jRc
om0YU9VBGdjLY6mBZqE3YQKCAQEA94YN6Ti6rohtNHJFmhx+eqyes8FcQ4iIwmBi
lYC4uBPD8gsNfpFzC3AJgIet0Ua20y9qVLflMRzyRvn51c6b+lq+nzQhFcBmTaZv
LAmo7eefaxGtTptn3kh8c/qw4SBcla5UUbU6AQiTl0rbu7HSxdNxVnIWci2L0wRE
4rzn9daONkfR03wAKVL3UGaGpG5AZtCxolVg0UK/xnRlJtONkMtPIOlKqAe0FQVz
x640bz9zhx1+YFHbuVAgcXaE8SFws35IEwOy0wse+5qcMTmF7D7IO5djxwbnQXOn
txmQYjcQPea7atDwqP7uSuxLlm8eS4H35/E0ej5NSdHXCZDhPwKCAQEA1VEoRUpt
ikRtv6+e9pJH18ypRMyX22F7rWHHKrDcIDQiq4e7gheQCR4jEYx4BoDysZA4Kj7k
yX4N+JsjUl67R3H8kkVCKcp5yEJenXQNYWfXm5FjUrBtYpPhtcUfJpvN2KJDC8SK
zEeVhdbyG7/AQdWmufCqlsclYXLTu9nUUoWx9r5EIgpX0qMbIwmFurrrU5K57VZq
Y3mjoQ5xyLOeXbyLw/0k6fPYPcMFcEop3x3RCliK7ZQbXtEeUT9lMKn51BnYIuzL
mUqefyEFijLTbjCxGOsHCrFWwG4ZHvX1hwQUCIyJ4KFyF2+HePdVJBV7xvoCvMqu
qi9eUy66yF4QiQKCAQEAkQ6/wkjbpoMPNKgvQASo3q2hCAkuT+mI9BjWTXU1S7NR
ptxxWRZWTIKpHujkpXuCkzr7XpCywViMZ6oBjxpY8JwTkGtVZFDv41vBVRaIKHwQ
QggG/8U1VuA2ByYIWLbFa3bRtVy2lHp5PDXDlji9I/ApQJb1ro4gnlanJBW+/qRx
jUADqKPH2G/lvfmHuH9gQKDXKSJN7fTnPJ9j1CAwm4V6ZvgxJ030bElYs9HUqsmN
o8ZWFI6EbRKXIMdrW4DUcmx0QqhtOR8QVjUiifqDEJMudzXeZHoYNpRX2ochxV0t
qIk6lnvCPdfAwKnAFnYiU+/j38rMjUNLuhI+qj18swKCAQEAmQQ9ce7gwrUxULZG
vjLSTC9oIIzuRZUY5ZdlCe+SB5u+XvLfg4MpdBDlMPplQq0O6mY8k7lH3m2I0hNe
9AA2MRrIBZiezGGMmCVbv3B3BK87j0FsUO1zmang18XOAaDzuPk0ROW4dq7GyBoW
0+bpn5AFqA3GIKA75To8Pd0UnBmBPjDfqNdohhUwKyLyha79jZaLvn8lk7O2t5on
7sNBz5WnYHBfIV8kd7FEXncksAKwZ8554dBDDfHN4JQsHwtexVHAo9Tee1h2d1UV
pzusRLey4d/9ziLFywVvCbF/bVl0kWlWogbNU5hSQmiQ73BluKawfgKZ5kRSUjMM
Exy+mQKCAQBIrmOVr5WJDtG5g78w0ALC4b9o+vVb8bW+jFjVULt4uyfMAq4HEdTw
69Sp6uNpZ+QFNUyUEMxdhEtfXliQipOLFtPLzF+U/x2izHyf4mp4I09b8kBUbt2H
t/ajOe3a66X4jk/PQGr6MkuZdPvz61KwxXMufgYTtHpMcdZXs3/baMaDly5F9CbN
YHseNkWS+pjRLEJ+SoiiBylmBCwm8FMkVKiTOu1/kHYg+IaDVOGK4SfNKAKA/zP6
DpkOGekQdLcv0DqDGzNQrGWpMCJ2CF3kq8USX9F3aHlQLvTrjWQDTTE5lNCaVo3N
4qTzfwHOQoia5dVH9t46UancuuVvC0vk
-----END PRIVATE KEY-----
@@ -1,16 +0,0 @@
saml:
container_name: saml
image: kristophjunge/test-saml-idp
environment:
SIMPLESAMLPHP_SP_ENTITY_ID: http://grafana.com
SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE: http://localhost/simplesaml/module.php/saml/sp/saml2-acs.php/test-sp
SIMPLESAMLPHP_SP_SINGLE_LOGOUT_SERVICE: http://localhost/simplesaml/module.php/saml/sp/saml2-logout.php/test-sp
SIMPLESAMLPHP_ADMIN_PASSWORD: grafana
SIMPLESAMLPHP_SECRET_SALT: salt
ports:
- "8080:8080"
- "8443:8443"
volumes:
- ./docker/blocks/auth/saml/users.php:/var/www/simplesamlphp/config/authsources.php
-16
View File
@@ -1,16 +0,0 @@
# Notes on Saml Docker Block
Uses https://github.com/kristophjunge/docker-test-saml-idp as a docker container for saml example.
## Use
See docker container docs on how to use this service - https://github.com/kristophjunge/docker-test-saml-idp#test-the-identity-provider-idp
## Groups & Users
admins
saml-admin (saml-admin@grafana.com)
editors
saml-editor (saml-editor@grafana.com)
no groups
saml-viewer (saml-viewer@grafana.com)
-21
View File
@@ -1,21 +0,0 @@
<?php
$config = array(
'admin' => array(
'core:AdminPassword',
),
'example-userpass' => array(
'exampleauth:UserPass',
'saml-admin:grafana' => array(
'groups' => array('admins'),
'email' => 'saml-admin@grafana.com',
),
'saml-editor:grafana' => array(
'groups' => array('editors'),
'email' => 'saml-editor@grafana.com',
),
'saml-viewer:grafana' => array(
'groups' => array(),
'email' => 'saml-viewer@grafana.com',
),
),
);
@@ -1,11 +1,12 @@
---
aliases:
- share-a-plugin/
title: Package a plugin
- package-a-plugin/
title: Publish a plugin
type: docs
---
# Package a plugin
# Publish a plugin
You've just built your first plugin, and now you want to share it with the world. In this guide, you'll learn how to package and share your plugin with others.
@@ -17,7 +18,7 @@ There are three steps needed to package a plugin:
- Building the plugin
- Signing the plugin
- Archiving the plugin
- Packaging the plugin
1. Build the plugin
@@ -87,6 +88,7 @@ To speed up the time it takes to review your plugin:
- Check that your plugin is ready for review using the [plugin validator](https://github.com/grafana/plugin-validator).
- Read our [6 tips for improving your Grafana plugin before you publish](https://grafana.com/blog/2021/01/21/6-tips-for-improving-your-grafana-plugin-before-you-publish/).
- Refer to [plugin-examples](https://github.com/grafana/grafana-plugin-examples) to review best practices for building your plugin.
## Frequently Asked Questions
@@ -98,9 +100,13 @@ To speed up the time it takes to review your plugin:
- We're not able to give an estimate at this time, though we're constantly working on improving the time it takes to review a plugin.
**Can I decide a date when my plugin will be published?**
- No. We cannot guarantee specific publishing dates, as plugins are immediately published after a review based on our internal prioritization.
## Publishing your plugin for the first time
{{< figure src="/static/img/docs/plugins/plugins-submission-create.png" class="docs-image--no-shadow" max-width="850px" >}}
{{< figure src="/static/img/docs/plugins/plugins-submission-create.png" class="docs-image--no-shadow" max-width="650px" >}}
1. [Sign in](https://grafana.com/auth/sign-in) to your Grafana Cloud account.
1. In the left menu, under **Org settings**, click **My Plugins**.
@@ -120,7 +126,7 @@ All submissions are manually inspected by a plugin reviewer. For every new plugi
- **Code review:** For quality and security purposes, we review the source code for the plugin. If you're unable to make the source code publicly available, let us know in a comment on your plugin submission.
- **Tests:** We install your plugin on one of our Grafana instances to test it for basic use. For more advanced plugins, we may ask you to assist us in configuring a test environment for the plugin. This test environment will be used whenever you submit an plugin update.
## Maintain your plugin
## Submit a plugin update
To submit an **update** for an already published plugin:
+1 -1
View File
@@ -60,7 +60,7 @@ require (
github.com/google/wire v0.5.0
github.com/gorilla/websocket v1.5.0
github.com/grafana/alerting v0.0.0-20221221211348-c5ab25d1cb8a
github.com/grafana/cuetsy v0.1.1
github.com/grafana/cuetsy v0.1.5
github.com/grafana/grafana-aws-sdk v0.11.0
github.com/grafana/grafana-azure-sdk-go v1.5.1
github.com/grafana/grafana-plugin-sdk-go v0.147.0
+2 -2
View File
@@ -1363,8 +1363,8 @@ github.com/grafana/alerting v0.0.0-20221221211348-c5ab25d1cb8a h1:Ros/f8x4rPGiOy
github.com/grafana/alerting v0.0.0-20221221211348-c5ab25d1cb8a/go.mod h1:A+ko8Ui4Ojw9oTi1WMCPH937mFUozN8Y41cqrOfNuy8=
github.com/grafana/codejen v0.0.3 h1:tAWxoTUuhgmEqxJPOLtJoxlPBbMULFwKFOcRsPRPXDw=
github.com/grafana/codejen v0.0.3/go.mod h1:zmwwM/DRyQB7pfuBjTWII3CWtxcXh8LTwAYGfDfpR6s=
github.com/grafana/cuetsy v0.1.1 h1:+1jaDDYCpvKlcOWJgBRbkc5+VZIClCEn5mbI+4PLZqM=
github.com/grafana/cuetsy v0.1.1/go.mod h1:4KWkUOslwvRTpEv7wdQG0jDFTuJmU+0L9x0h4kWxa2A=
github.com/grafana/cuetsy v0.1.5 h1:mnFwAXdbqCsyL8r7kkdUMJ4kOAR26cxIPmrZj7JzTeY=
github.com/grafana/cuetsy v0.1.5/go.mod h1:4KWkUOslwvRTpEv7wdQG0jDFTuJmU+0L9x0h4kWxa2A=
github.com/grafana/dskit v0.0.0-20211011144203-3a88ec0b675f h1:FvvSVEbnGeM2bUivGmsiXTi8URJyBU7TcFEEoRe5wWI=
github.com/grafana/dskit v0.0.0-20211011144203-3a88ec0b675f/go.mod h1:uPG2nyK4CtgNDmWv7qyzYcdI+S90kHHRWvHnBtEMBXM=
github.com/grafana/go-mssqldb v0.0.0-20210326084033-d0ce3c521036 h1:GplhUk6Xes5JIhUUrggPcPBhOn+eT8+WsHiebvq7GgA=
+2 -2
View File
@@ -21,13 +21,13 @@ lineage: seqs: [
// TODO - it seems it's a team_member.permission, unlikely it should belong to the team kind
permission: #Permission @grafanamaturity(ToMetadata="kind", MaybeRemove)
// AccessControl metadata associated with a given resource.
accessControl: [string]: bool @grafanamaturity(ToMetadata="sys")
accessControl?: [string]: bool @grafanamaturity(ToMetadata="sys")
// Created indicates when the team was created.
created: int64 @grafanamaturity(ToMetadata="sys")
// Updated indicates when the team was updated.
updated: int64 @grafanamaturity(ToMetadata="sys")
#Permission: 1 | 2 | 4 @cuetsy(kind="enum",memberNames="viewer|editor|admin")
#Permission: 1 | 2 | 4 @cuetsy(kind="enum",memberNames="Viewer|Editor|Admin")
},
]
},
+2 -2
View File
@@ -78,7 +78,7 @@
"@babel/plugin-proposal-class-properties": "7.18.6",
"@babel/plugin-proposal-nullish-coalescing-operator": "7.18.6",
"@babel/plugin-proposal-object-rest-spread": "7.20.2",
"@babel/plugin-proposal-optional-chaining": "7.18.9",
"@babel/plugin-proposal-optional-chaining": "7.20.7",
"@babel/plugin-syntax-dynamic-import": "7.8.3",
"@babel/plugin-transform-react-constant-elements": "7.20.2",
"@babel/plugin-transform-runtime": "7.19.6",
@@ -282,7 +282,7 @@
"@sentry/utils": "6.19.7",
"@types/react-resizable": "3.0.3",
"@types/webpack-env": "1.18.0",
"@visx/event": "2.6.0",
"@visx/event": "2.17.0",
"@visx/gradient": "2.10.0",
"@visx/group": "2.10.0",
"@visx/scale": "2.2.2",
@@ -102,9 +102,9 @@ export const defaultVariableModel: Partial<VariableModel> = {
};
export enum VariableHide {
DontHide = 0,
HideLabel = 1,
HideVariable = 2,
dontHide = 0,
hideLabel = 1,
hideVariable = 2,
}
export enum LoadingState {
@@ -287,7 +287,7 @@ export enum MappingType {
* TODO docs
*/
export interface ValueMap {
options: Record<string, unknown>;
options: Record<string, ValueMappingResult>;
type: MappingType.ValueToText;
}
@@ -447,7 +447,7 @@ export interface Panel {
/**
* TODO docs - seems to be an old field from old dashboard alerts?
*/
thresholds?: Array<any>;
thresholds?: Array<unknown>;
/**
* TODO docs
* TODO tighter constraint
@@ -456,7 +456,7 @@ export interface Panel {
/**
* TODO docs
*/
timeRegions?: Array<any>;
timeRegions?: Array<unknown>;
/**
* TODO docs
* TODO tighter constraint
@@ -494,7 +494,7 @@ export interface FieldConfigSource {
matcher: MatcherConfig;
properties: Array<{
id: string;
value?: any;
value?: unknown;
}>;
}>;
}
@@ -505,7 +505,7 @@ export const defaultFieldConfigSource: Partial<FieldConfigSource> = {
export interface MatcherConfig {
id: string;
options?: any;
options?: unknown;
}
export const defaultMatcherConfig: Partial<MatcherConfig> = {
@@ -546,7 +546,7 @@ export interface FieldConfig {
/**
* The behavior when clicking on a result
*/
links?: Array<any>;
links?: Array<unknown>;
/**
* Convert input values into a display string
*/
@@ -18,7 +18,7 @@ export interface Team {
/**
* AccessControl metadata associated with a given resource.
*/
accessControl: Record<string, unknown>;
accessControl?: Record<string, boolean>;
/**
* AvatarUrl is the team's avatar URL.
*/
@@ -72,7 +72,13 @@ export class UnthemedCanvasSpanGraph extends React.PureComponent<CanvasSpanGraph
}
render() {
return <canvas className={getStyles(this.props.theme).CanvasSpanGraph} ref={this._setCanvasRef} />;
return (
<canvas
className={getStyles(this.props.theme).CanvasSpanGraph}
ref={this._setCanvasRef}
data-testid="CanvasSpanGraph"
/>
);
}
}
@@ -56,5 +56,9 @@ export default function TickLabels(props: TickLabelsProps) {
);
}
return <div className={styles.TickLabels}>{ticks}</div>;
return (
<div className={styles.TickLabels} data-testid="TickLabels">
{ticks}
</div>
);
}
@@ -12,23 +12,21 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { shallow, ShallowWrapper } from 'enzyme';
import { getAllByTestId, render, screen } from '@testing-library/react';
import React from 'react';
import traceGenerator from '../../demo/trace-generators';
import transformTraceData from '../../model/transform-trace-data';
import { polyfill as polyfillAnimationFrame } from '../../utils/test/requestAnimationFrame';
import CanvasSpanGraph from './CanvasSpanGraph';
import TickLabels from './TickLabels';
import ViewingLayer, { ViewingLayerProps, UnthemedViewingLayer } from './ViewingLayer';
import SpanGraph, { SpanGraphProps } from './index';
import SpanGraph, { SpanGraphProps, TIMELINE_TICK_INTERVAL } from './index';
describe('<SpanGraph>', () => {
polyfillAnimationFrame(window);
const trace = transformTraceData(traceGenerator.trace({}));
const trace = transformTraceData(traceGenerator.trace({}))!;
const props = {
trace,
updateViewRangeTime: () => {},
@@ -39,52 +37,35 @@ describe('<SpanGraph>', () => {
},
};
let wrapper: ShallowWrapper<ViewingLayerProps, {}, UnthemedViewingLayer>;
beforeEach(() => {
wrapper = shallow(<SpanGraph {...(props as unknown as SpanGraphProps)} />);
render(<SpanGraph {...(props as unknown as SpanGraphProps)} />);
});
it('renders a <CanvasSpanGraph />', () => {
expect(wrapper.find(CanvasSpanGraph).length).toBe(1);
it('renders <CanvasSpanGraph />', () => {
const canvasSpanGraphComponent = screen.getByTestId('CanvasSpanGraph');
expect(canvasSpanGraphComponent).toBeTruthy();
});
it('renders a <TickLabels />', () => {
expect(wrapper.find(TickLabels).length).toBe(1);
it('renders <TickLabels />', () => {
const tickLabelsComponent = screen.getByTestId('TickLabels');
expect(tickLabelsComponent).toBeTruthy();
});
it('returns a <div> if a trace is not provided', () => {
wrapper = shallow(<SpanGraph {...({ ...props, trace: null } as unknown as SpanGraphProps)} />);
expect(wrapper.matchesElement(<div />)).toBeTruthy();
it('returns an empty div if a trace is not provided', () => {
const { container } = render(<SpanGraph {...({ ...props, trace: null } as unknown as SpanGraphProps)} />);
expect(container.firstChild).toBeEmptyDOMElement();
});
it('passes the number of ticks to render to components', () => {
const tickHeader = wrapper.find(TickLabels);
const viewingLayer = wrapper.find(ViewingLayer);
expect(tickHeader.prop('numTicks')).toBeGreaterThan(1);
expect(viewingLayer.prop('numTicks')).toBeGreaterThan(1);
expect(tickHeader.prop('numTicks')).toBe(viewingLayer.prop('numTicks'));
it('renders <TickLabels /> with the correct numnber of ticks', async () => {
const tickLabelsDiv = screen.getByTestId('TickLabels');
expect(getAllByTestId(tickLabelsDiv, 'tick').length).toBe(TIMELINE_TICK_INTERVAL + 1);
});
it('passes items to CanvasSpanGraph', () => {
const canvasGraph = wrapper.find(CanvasSpanGraph).first();
const items = trace?.spans.map((span) => ({
valueOffset: span.relativeStartTime,
valueWidth: span.duration,
serviceName: span.process.serviceName,
}));
expect(canvasGraph.prop('items')).toEqual(items);
});
it('does not regenerate CanvasSpanGraph without new trace', () => {
const canvasGraph = wrapper.find(CanvasSpanGraph).first();
const items = canvasGraph.prop('items');
wrapper.instance().forceUpdate();
const newCanvasGraph = wrapper.find(CanvasSpanGraph).first();
const newItems = newCanvasGraph.prop('items');
expect(newItems).toBe(items);
it('renders <TickLabels /> with the correct value next to each tick', () => {
render(<TickLabels numTicks={5} duration={1} />);
expect(screen.getByText(/0.2/)).toBeTruthy();
expect(screen.getByText(/0.4/)).toBeTruthy();
expect(screen.getByText(/0.6/)).toBeTruthy();
expect(screen.getByText(/0.8/)).toBeTruthy();
});
});
@@ -25,7 +25,7 @@ import TickLabels from './TickLabels';
import ViewingLayer from './ViewingLayer';
const DEFAULT_HEIGHT = 60;
const TIMELINE_TICK_INTERVAL = 4;
export const TIMELINE_TICK_INTERVAL = 4;
export type SpanGraphProps = {
height?: number;
@@ -13,14 +13,15 @@
// limitations under the License.
import cx from 'classnames';
import { mount } from 'enzyme';
import { mount, ReactWrapper } from 'enzyme';
import React from 'react';
import DraggableManager, { DraggingUpdate } from 'src/utils/DraggableManager';
import TimelineColumnResizer, { getStyles } from './TimelineColumnResizer';
import TimelineColumnResizer, { getStyles, TimelineColumnResizerProps } from './TimelineColumnResizer';
describe('<TimelineColumnResizer>', () => {
let wrapper;
let instance;
let wrapper: ReactWrapper<TimelineColumnResizerProps, {}, TimelineColumnResizer>;
let instance: TimelineColumnResizer;
const props = {
min: 0.1,
@@ -31,7 +32,7 @@ describe('<TimelineColumnResizer>', () => {
beforeEach(() => {
props.onChange.mockReset();
wrapper = mount(<TimelineColumnResizer {...props} />);
wrapper = mount(<TimelineColumnResizer {...(props as unknown as TimelineColumnResizerProps)} />);
instance = wrapper.instance();
});
@@ -57,7 +58,7 @@ describe('<TimelineColumnResizer>', () => {
it('returns the draggable bounds via _getDraggingBounds()', () => {
const left = 10;
const width = 100;
instance._rootElm.getBoundingClientRect = () => ({ left, width });
instance._rootElm!.getBoundingClientRect = () => ({ left, width } as DOMRect);
expect(instance._getDraggingBounds()).toEqual({
width,
clientXLeft: left,
@@ -69,16 +70,16 @@ describe('<TimelineColumnResizer>', () => {
it('handles drag start', () => {
const value = Math.random();
expect(wrapper.state('dragPosition')).toBe(null);
instance._handleDragUpdate({ value });
instance._handleDragUpdate({ value } as DraggingUpdate);
expect(wrapper.state('dragPosition')).toBe(value);
});
it('handles drag end', () => {
const manager = { resetBounds: jest.fn() };
const manager = { resetBounds: jest.fn() } as unknown as DraggableManager;
const value = Math.random();
wrapper.setState({ dragPosition: 2 * value });
instance._handleDragEnd({ manager, value });
expect(manager.resetBounds.mock.calls).toEqual([[]]);
instance._handleDragEnd({ manager, value } as DraggingUpdate);
expect(jest.mocked(manager.resetBounds).mock.calls).toEqual([[]]);
expect(wrapper.state('dragPosition')).toBe(null);
expect(props.onChange.mock.calls).toEqual([[value]]);
});
@@ -86,16 +87,16 @@ describe('<TimelineColumnResizer>', () => {
it('does not render a dragging indicator when not dragging', () => {
const styles = getStyles();
expect(wrapper.find('[data-testid="TimelineColumnResizer--dragger"]').prop('style').right).toBe(undefined);
expect(wrapper.find('[data-testid="TimelineColumnResizer--dragger"]').prop('style')?.right).toBe(undefined);
expect(wrapper.find('[data-testid="TimelineColumnResizer--dragger"]').prop('className')).toBe(styles.dragger);
});
it('renders a dragging indicator when dragging', () => {
instance._dragManager.isDragging = () => true;
instance._handleDragUpdate({ value: props.min });
instance._handleDragUpdate({ value: props.min } as DraggingUpdate);
instance.forceUpdate();
wrapper.update();
expect(wrapper.find('[data-testid="TimelineColumnResizer--dragger"]').prop('style').right).toBeDefined();
expect(wrapper.find('[data-testid="TimelineColumnResizer--dragger"]').prop('style')?.right).toBeDefined();
const styles = getStyles();
expect(wrapper.find('[data-testid="TimelineColumnResizer--dragger"]').prop('className')).toBe(
@@ -97,7 +97,7 @@ export const getStyles = stylesFactory(() => {
};
});
type TimelineColumnResizerProps = {
export type TimelineColumnResizerProps = {
min: number;
max: number;
onChange: (newSize: number) => void;
@@ -15,7 +15,7 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import TimelineHeaderRow from './TimelineHeaderRow';
import TimelineHeaderRow, { TimelineHeaderRowProps } from './TimelineHeaderRow';
const nameColumnWidth = 0.25;
const setup = () => {
@@ -35,7 +35,7 @@ const setup = () => {
},
};
return render(<TimelineHeaderRow {...props} />);
return render(<TimelineHeaderRow {...(props as unknown as TimelineHeaderRowProps)} />);
};
describe('TimelineHeaderRow', () => {
@@ -56,7 +56,7 @@ const getStyles = (theme: GrafanaTheme2) => {
};
};
type TimelineHeaderRowProps = {
export type TimelineHeaderRowProps = {
duration: number;
nameColumnWidth: number;
numTicks: number;
@@ -13,18 +13,29 @@
// limitations under the License.
import { cx } from '@emotion/css';
import { mount } from 'enzyme';
import { mount, ReactWrapper } from 'enzyme';
import React from 'react';
import DraggableManager, { DraggingUpdate } from 'src/utils/DraggableManager';
import TimelineViewingLayer, { getStyles } from './TimelineViewingLayer';
import { ViewRangeTime } from '../types';
function mapFromSubRange(viewStart, viewEnd, value) {
import TimelineViewingLayer, { getStyles, TimelineViewingLayerProps } from './TimelineViewingLayer';
function mapFromSubRange(viewStart: number, viewEnd: number, value: number) {
return viewStart + value * (viewEnd - viewStart);
}
// Remove the prefix generated by cx (css-<id>), i.e. change the string from something like:
// 'css-7g92us-TimelineViewingLayerDragged-Timeline...'
// to:
// 'TimelineViewingLayerDragged-Timeline...'
function removeCxPrefix(cxString: string) {
return cxString.split('-').slice(2).join('-');
}
describe('<TimelineViewingLayer>', () => {
let wrapper;
let instance;
let wrapper: ReactWrapper<TimelineViewingLayerProps, {}, TimelineViewingLayer>;
let instance: TimelineViewingLayer;
const viewStart = 0.25;
const viewEnd = 0.9;
@@ -33,14 +44,14 @@ describe('<TimelineViewingLayer>', () => {
updateNextViewRangeTime: jest.fn(),
updateViewRangeTime: jest.fn(),
viewRangeTime: {
current: [viewStart, viewEnd],
current: [viewStart, viewEnd] as [number, number],
},
};
beforeEach(() => {
props.updateNextViewRangeTime.mockReset();
props.updateViewRangeTime.mockReset();
wrapper = mount(<TimelineViewingLayer {...props} />);
wrapper = mount(<TimelineViewingLayer {...(props as unknown as TimelineViewingLayerProps)} />);
instance = wrapper.instance();
});
@@ -76,19 +87,19 @@ describe('<TimelineViewingLayer>', () => {
it('returns the dragging bounds from _getDraggingBounds()', () => {
const left = 10;
const width = 100;
instance._root.getBoundingClientRect = () => ({ left, width });
instance._root!.getBoundingClientRect = () => ({ left, width } as DOMRect);
expect(instance._getDraggingBounds()).toEqual({ width, clientXLeft: left });
});
it('updates viewRange.time.cursor via _draggerReframe._onMouseMove', () => {
const value = 0.5;
const cursor = mapFromSubRange(viewStart, viewEnd, value);
instance._draggerReframe._onMouseMove({ value });
instance._draggerReframe._onMouseMove!({ value } as DraggingUpdate);
expect(props.updateNextViewRangeTime.mock.calls).toEqual([[{ cursor }]]);
});
it('resets viewRange.time.cursor via _draggerReframe._onMouseLeave', () => {
instance._draggerReframe._onMouseLeave();
instance._draggerReframe._onMouseLeave!(null as unknown as DraggingUpdate);
expect(props.updateNextViewRangeTime.mock.calls).toEqual([[{ cursor: undefined }]]);
});
@@ -96,33 +107,33 @@ describe('<TimelineViewingLayer>', () => {
const value = 0.5;
const shift = mapFromSubRange(viewStart, viewEnd, value);
const update = { reframe: { shift, anchor: shift } };
instance._draggerReframe._onDragStart({ value });
instance._draggerReframe._onDragStart!({ value } as DraggingUpdate);
expect(props.updateNextViewRangeTime.mock.calls).toEqual([[update]]);
});
it('handles drag move via _draggerReframe._onDragMove', () => {
const anchor = 0.25;
const viewRangeTime = { ...props.viewRangeTime, reframe: { anchor, shift: Math.random() } };
const viewRangeTime = { ...props.viewRangeTime, reframe: { anchor, shift: Math.random() } } as ViewRangeTime;
const value = 0.5;
const shift = mapFromSubRange(viewStart, viewEnd, value);
// make sure `anchor` is already present on the props
wrapper.setProps({ viewRangeTime });
expect(wrapper.prop('viewRangeTime').reframe.anchor).toBe(anchor);
expect(wrapper.prop('viewRangeTime').reframe?.anchor).toBe(anchor);
// the next update should integrate `value` and use the existing anchor
instance._draggerReframe._onDragStart({ value });
instance._draggerReframe._onDragStart!({ value } as DraggingUpdate);
const update = { reframe: { anchor, shift } };
expect(props.updateNextViewRangeTime.mock.calls).toEqual([[update]]);
});
it('handles drag end via _draggerReframe._onDragEnd', () => {
const manager = { resetBounds: jest.fn() };
const manager = { resetBounds: jest.fn() } as unknown as DraggableManager;
const value = 0.5;
const shift = mapFromSubRange(viewStart, viewEnd, value);
const anchor = 0.25;
const viewRangeTime = { ...props.viewRangeTime, reframe: { anchor, shift: Math.random() } };
const viewRangeTime = { ...props.viewRangeTime, reframe: { anchor, shift: Math.random() } } as ViewRangeTime;
wrapper.setProps({ viewRangeTime });
instance._draggerReframe._onDragEnd({ manager, value });
expect(manager.resetBounds.mock.calls).toEqual([[]]);
instance._draggerReframe._onDragEnd!({ manager, value } as DraggingUpdate);
expect(jest.mocked(manager.resetBounds).mock.calls).toEqual([[]]);
expect(props.updateViewRangeTime.mock.calls).toEqual([[anchor, shift, 'timeline-header']]);
});
});
@@ -139,7 +150,12 @@ describe('<TimelineViewingLayer>', () => {
// cursor is rendered when solo
expect(wrapper.find('[data-testid="TimelineViewingLayer--cursorGuide"]').length).toBe(1);
// cursor is skipped when shiftStart, shiftEnd, or reframe are present
let viewRangeTime = { ...baseViewRangeTime, shiftStart: cursor };
let viewRangeTime: ViewRangeTime = {
...baseViewRangeTime,
shiftStart: cursor,
shiftEnd: cursor,
reframe: { anchor: cursor, shift: cursor },
};
wrapper.setProps({ viewRangeTime });
expect(wrapper.find('[data-testid="TimelineViewingLayer--cursorGuide"]').length).toBe(0);
viewRangeTime = { ...baseViewRangeTime, shiftEnd: cursor };
@@ -157,9 +173,16 @@ describe('<TimelineViewingLayer>', () => {
expect(
wrapper
.find('[data-testid="Dragged"]')
.prop('className')
.prop('className')!
.indexOf(
cx(styles.dragged, styles.draggedDraggingLeft, styles.draggedDraggingRight, styles.draggedReframeDrag)
removeCxPrefix(
cx(
styles.TimelineViewingLayerDragged,
styles.TimelineViewingLayerDraggedDraggingLeft,
styles.TimelineViewingLayerDraggedDraggingRight,
styles.TimelineViewingLayerDraggedReframeDrag
)
)
) >= 0
).toBe(true);
});
@@ -171,9 +194,16 @@ describe('<TimelineViewingLayer>', () => {
expect(
wrapper
.find('[data-testid="Dragged"]')
.prop('className')
.prop('className')!
.indexOf(
cx(styles.dragged, styles.draggedDraggingLeft, styles.draggedDraggingRight, styles.draggedShiftDrag)
removeCxPrefix(
cx(
styles.TimelineViewingLayerDragged,
styles.TimelineViewingLayerDraggedDraggingLeft,
styles.TimelineViewingLayerDraggedDraggingRight,
styles.TimelineViewingLayerDraggedShiftDrag
)
)
) >= 0
).toBe(true);
});
@@ -181,13 +211,20 @@ describe('<TimelineViewingLayer>', () => {
it('renders the shiftEnd dragging', () => {
const viewRangeTime = { ...props.viewRangeTime, shiftEnd: viewStart };
wrapper.setProps({ viewRangeTime });
// expect(wrapper.find('.isDraggingLeft.isShiftDrag').length).toBe(1);
const styles = getStyles();
expect(
wrapper
.find('[data-testid="Dragged"]')
.prop('className')
.indexOf(cx(styles.dragged, styles.draggedDraggingLeft, styles.draggedShiftDrag)) >= 0
.prop('className')!
.indexOf(
removeCxPrefix(
cx(
styles.TimelineViewingLayerDragged,
styles.TimelineViewingLayerDraggedDraggingLeft,
styles.TimelineViewingLayerDraggedShiftDrag
)
)
) >= 0
).toBe(true);
});
});
@@ -79,7 +79,7 @@ export const getStyles = stylesFactory(() => {
};
});
type TimelineViewingLayerProps = {
export type TimelineViewingLayerProps = {
/**
* `boundsInvalidator` is an arbitrary prop that lets the component know the
* bounds for dragging need to be recalculated. In practice, the name column
+1 -2
View File
@@ -9,7 +9,6 @@ import (
"errors"
"fmt"
"net/http"
"net/url"
"golang.org/x/oauth2"
@@ -258,7 +257,7 @@ func (hs *HTTPServer) OAuthLogin(ctx *models.ReqContext) {
hs.HooksService.RunLoginHook(&loginInfo, ctx)
metrics.MApiLoginOAuth.Inc()
if redirectTo, err := url.QueryUnescape(ctx.GetCookie("redirect_to")); err == nil && len(redirectTo) > 0 {
if redirectTo := ctx.GetCookie("redirect_to"); len(redirectTo) > 0 {
if err := hs.ValidateRedirectTo(redirectTo); err == nil {
cookies.DeleteCookie(ctx.Resp, "redirect_to", hs.CookieOptionsFromCfg)
ctx.Redirect(redirectTo)
+1 -1
View File
@@ -271,7 +271,7 @@ func (hs *HTTPServer) GetPluginMarkdown(c *models.ReqContext) response.Response
// fallback try readme
if len(content) == 0 {
content, err = hs.pluginMarkdown(c.Req.Context(), pluginID, "help")
content, err = hs.pluginMarkdown(c.Req.Context(), pluginID, "readme")
if err != nil {
return response.Error(501, "Could not get markdown file", err)
}
+1 -1
View File
@@ -220,7 +220,7 @@ func DecoderCompactor() dstutil.ApplyFunc {
c.Delete()
}
case *dst.Field:
if id, is := x.Type.(*dst.Ident); is {
if id, is := ddepoint(x.Type).(*dst.Ident); is {
if expr, has := replace[id.Name]; has {
x.Type = expr
}
+1 -1
View File
@@ -25,7 +25,7 @@ type Permission int
// Team defines model for team.
type Team struct {
// AccessControl metadata associated with a given resource.
AccessControl map[string]bool `json:"accessControl"`
AccessControl map[string]bool `json:"accessControl,omitempty"`
// AvatarUrl is the team's avatar URL.
AvatarUrl *string `json:"avatarUrl,omitempty"`
+19 -9
View File
@@ -2,6 +2,7 @@ package authn
import (
"context"
"fmt"
"net/http"
"strconv"
"strings"
@@ -16,6 +17,7 @@ import (
const (
ClientAPIKey = "auth.client.api-key" // #nosec G101
ClientAnonymous = "auth.client.anonymous"
ClientBasic = "auth.client.basic"
)
type ClientParams struct {
@@ -48,8 +50,9 @@ type Request struct {
}
const (
APIKeyIDPrefix = "api-key:"
ServiceAccountIDPrefix = "service-account:"
NamespaceUser = "user"
NamespaceAPIKey = "api-key"
NamespaceServiceAccount = "service-account"
)
type Identity struct {
@@ -105,6 +108,11 @@ func (i *Identity) NamespacedID() (string, int64) {
}
func (i *Identity) SignedInUser() *user.SignedInUser {
var isGrafanaAdmin bool
if i.IsGrafanaAdmin != nil {
isGrafanaAdmin = *i.IsGrafanaAdmin
}
u := &user.SignedInUser{
UserID: 0,
OrgID: i.OrgID,
@@ -116,7 +124,7 @@ func (i *Identity) SignedInUser() *user.SignedInUser {
Name: i.Name,
Email: i.Email,
OrgCount: i.OrgCount,
IsGrafanaAdmin: *i.IsGrafanaAdmin,
IsGrafanaAdmin: isGrafanaAdmin,
IsAnonymous: i.IsAnonymous(),
IsDisabled: i.IsDisabled,
HelpFlags1: i.HelpFlags1,
@@ -124,19 +132,21 @@ func (i *Identity) SignedInUser() *user.SignedInUser {
Teams: i.Teams,
}
// For now, we need to set different fields of the signed-in user based on the identity "type"
if strings.HasPrefix(i.ID, APIKeyIDPrefix) {
id, _ := strconv.ParseInt(strings.TrimPrefix(i.ID, APIKeyIDPrefix), 10, 64)
namespace, id := i.NamespacedID()
if namespace == NamespaceAPIKey {
u.ApiKeyID = id
} else if strings.HasPrefix(i.ID, ServiceAccountIDPrefix) {
id, _ := strconv.ParseInt(strings.TrimPrefix(i.ID, ServiceAccountIDPrefix), 10, 64)
} else {
u.UserID = id
u.IsServiceAccount = true
u.IsServiceAccount = namespace == NamespaceServiceAccount
}
return u
}
func NamespacedID(namespace string, id int64) string {
return fmt.Sprintf("%s:%d", namespace, id)
}
func IdentityFromSignedInUser(id string, usr *user.SignedInUser) *Identity {
return &Identity{
ID: id,
+16 -3
View File
@@ -7,11 +7,15 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/apikey"
"github.com/grafana/grafana/pkg/services/authn"
sync "github.com/grafana/grafana/pkg/services/authn/authnimpl/usersync"
"github.com/grafana/grafana/pkg/services/authn/clients"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/loginattempt"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"go.opentelemetry.io/otel/attribute"
@@ -20,7 +24,11 @@ import (
// make sure service implements authn.Service interface
var _ authn.Service = new(Service)
func ProvideService(cfg *setting.Cfg, tracer tracing.Tracer, orgService org.Service, apikeyService apikey.Service, userService user.Service) *Service {
func ProvideService(
cfg *setting.Cfg, tracer tracing.Tracer, orgService org.Service, accessControlService accesscontrol.Service,
apikeyService apikey.Service, userService user.Service, loginAttempts loginattempt.Service, quotaService quota.Service,
authInfoService login.AuthInfoService,
) *Service {
s := &Service{
log: log.New("authn.service"),
cfg: cfg,
@@ -35,9 +43,14 @@ func ProvideService(cfg *setting.Cfg, tracer tracing.Tracer, orgService org.Serv
s.clients[authn.ClientAnonymous] = clients.ProvideAnonymous(cfg, orgService)
}
// FIXME (kalleep): handle cfg.DisableLogin as well?
if s.cfg.BasicAuthEnabled && !s.cfg.DisableLogin {
s.clients[authn.ClientBasic] = clients.ProvideBasic(userService, loginAttempts)
}
// FIXME (jguer): move to User package
userSyncService := &sync.UserSync{}
orgUserSyncService := &sync.OrgSync{}
userSyncService := sync.ProvideUserSync(userService, authInfoService, quotaService)
orgUserSyncService := sync.ProvideOrgSync(userService, orgService, accessControlService)
s.RegisterPostAuthHook(userSyncService.SyncUser)
s.RegisterPostAuthHook(orgUserSyncService.SyncOrgUser)
@@ -15,6 +15,10 @@ import (
"github.com/grafana/grafana/pkg/services/user"
)
func ProvideOrgSync(userService user.Service, orgService org.Service, accessControl accesscontrol.Service) *OrgSync {
return &OrgSync{userService, orgService, accessControl, log.New("org.sync")}
}
type OrgSync struct {
userService user.Service
orgService org.Service
@@ -13,6 +13,10 @@ import (
"github.com/grafana/grafana/pkg/services/user"
)
func ProvideUserSync(userService user.Service, authInfoService login.AuthInfoService, quotaService quota.Service) *UserSync {
return &UserSync{userService, authInfoService, quotaService, log.New("user.sync")}
}
type UserSync struct {
userService user.Service
authInfoService login.AuthInfoService
+6 -16
View File
@@ -3,7 +3,6 @@ package clients
import (
"context"
"errors"
"fmt"
"strings"
"time"
@@ -18,11 +17,6 @@ import (
"github.com/grafana/grafana/pkg/util/errutil"
)
const (
basicPrefix = "Basic "
bearerPrefix = "Bearer "
)
var (
ErrAPIKeyInvalid = errutil.NewBase(errutil.StatusUnauthorized, "api-key.invalid", errutil.WithPublicMessage("Invalid API key"))
ErrAPIKeyExpired = errutil.NewBase(errutil.StatusUnauthorized, "api-key.expired", errutil.WithPublicMessage("Expired API key"))
@@ -46,14 +40,6 @@ type APIKey struct {
apiKeyService apikey.Service
}
func (s *APIKey) ClientParams() *authn.ClientParams {
return &authn.ClientParams{
SyncUser: false,
AllowSignUp: false,
EnableDisabledUsers: false,
}
}
func (s *APIKey) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) {
apiKey, err := s.getAPIKey(ctx, getTokenFromRequest(r))
if err != nil {
@@ -85,7 +71,7 @@ func (s *APIKey) Authenticate(ctx context.Context, r *authn.Request) (*authn.Ide
// if the api key don't belong to a service account construct the identity and return it
if apiKey.ServiceAccountId == nil || *apiKey.ServiceAccountId < 1 {
return &authn.Identity{
ID: fmt.Sprintf("%s%d", authn.APIKeyIDPrefix, apiKey.Id),
ID: authn.NamespacedID(authn.NamespaceAPIKey, apiKey.Id),
OrgID: apiKey.OrgId,
OrgRoles: map[int64]org.RoleType{apiKey.OrgId: apiKey.Role},
}, nil
@@ -104,7 +90,7 @@ func (s *APIKey) Authenticate(ctx context.Context, r *authn.Request) (*authn.Ide
return nil, ErrServiceAccountDisabled.Errorf("Disabled service account")
}
return authn.IdentityFromSignedInUser(fmt.Sprintf("%s%d", authn.ServiceAccountIDPrefix, *apiKey.ServiceAccountId), usr), nil
return authn.IdentityFromSignedInUser(authn.NamespacedID(authn.NamespaceServiceAccount, usr.UserID), usr), nil
}
func (s *APIKey) getAPIKey(ctx context.Context, token string) (*apikey.APIKey, error) {
@@ -159,6 +145,10 @@ func (s *APIKey) getFromTokenLegacy(ctx context.Context, token string) (*apikey.
return keyQuery.Result, nil
}
func (s *APIKey) ClientParams() *authn.ClientParams {
return &authn.ClientParams{}
}
func (s *APIKey) Test(ctx context.Context, r *authn.Request) bool {
return looksLikeApiKey(getTokenFromRequest(r))
}
+105
View File
@@ -0,0 +1,105 @@
package clients
import (
"context"
"crypto/subtle"
"strings"
"github.com/grafana/grafana/pkg/services/authn"
"github.com/grafana/grafana/pkg/services/loginattempt"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/util/errutil"
)
var (
ErrBasicAuthCredentials = errutil.NewBase(errutil.StatusUnauthorized, "basic-auth.invalid-credentials", errutil.WithPublicMessage("Invalid username or password"))
ErrDecodingBasicAuthHeader = errutil.NewBase(errutil.StatusBadRequest, "basic-auth.invalid-header", errutil.WithPublicMessage("Invalid Basic Auth Header"))
)
var _ authn.Client = new(Basic)
func ProvideBasic(userService user.Service, loginAttempts loginattempt.Service) *Basic {
return &Basic{userService, loginAttempts}
}
type Basic struct {
userService user.Service
loginAttempts loginattempt.Service
}
func (c *Basic) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) {
username, password, err := util.DecodeBasicAuthHeader(getBasicAuthHeaderFromRequest(r))
if err != nil {
return nil, ErrDecodingBasicAuthHeader.Errorf("failed to decode basic auth header: %w", err)
}
ok, err := c.loginAttempts.Validate(ctx, username)
if err != nil {
return nil, err
}
if !ok {
return nil, ErrBasicAuthCredentials.Errorf("too many consecutive incorrect login attempts for user - login for user temporarily blocked")
}
if len(password) == 0 {
return nil, ErrBasicAuthCredentials.Errorf("no password provided")
}
// FIXME (kalleep): decide if we should handle ldap here
usr, err := c.userService.GetByLogin(ctx, &user.GetUserByLoginQuery{LoginOrEmail: username})
if err != nil {
return nil, ErrBasicAuthCredentials.Errorf("failed to fetch user: %w", err)
}
if ok := comparePassword(password, usr.Salt, usr.Password); !ok {
_ = c.loginAttempts.Add(ctx, username, r.HTTPRequest.RemoteAddr)
return nil, ErrBasicAuthCredentials.Errorf("incorrect password provided")
}
signedInUser, err := c.userService.GetSignedInUserWithCacheCtx(ctx, &user.GetSignedInUserQuery{
UserID: usr.ID,
OrgID: r.OrgID,
})
if err != nil {
return nil, ErrBasicAuthCredentials.Errorf("failed to fetch user: %w", err)
}
return authn.IdentityFromSignedInUser(authn.NamespacedID(authn.NamespaceUser, signedInUser.UserID), signedInUser), nil
}
func (c *Basic) ClientParams() *authn.ClientParams {
return &authn.ClientParams{}
}
func (c *Basic) Test(ctx context.Context, r *authn.Request) bool {
return looksLikeBasicAuthRequest(r)
}
func looksLikeBasicAuthRequest(r *authn.Request) bool {
return getBasicAuthHeaderFromRequest(r) != ""
}
func getBasicAuthHeaderFromRequest(r *authn.Request) string {
if r.HTTPRequest == nil {
return ""
}
header := r.HTTPRequest.Header.Get(authorizationHeaderName)
if header == "" {
return ""
}
if !strings.HasPrefix(header, basicPrefix) {
return ""
}
return header
}
func comparePassword(password, salt, hash string) bool {
// It is ok to ignore the error here because util.EncodePassword can never return a error
hashedPassword, _ := util.EncodePassword(password, salt)
return subtle.ConstantTimeCompare([]byte(hashedPassword), []byte(hash)) == 1
}
+121
View File
@@ -0,0 +1,121 @@
package clients
import (
"context"
"net/http"
"testing"
"github.com/grafana/grafana/pkg/services/authn"
"github.com/grafana/grafana/pkg/services/loginattempt/loginattempttest"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/services/user/usertest"
"github.com/grafana/grafana/pkg/util"
"github.com/stretchr/testify/assert"
)
func TestBasic_Authenticate(t *testing.T) {
type TestCase struct {
desc string
req *authn.Request
blockLogin bool
expectedErr error
expectedSignedInUser *user.SignedInUser
expectedIdentity *authn.Identity
}
tests := []TestCase{
{
desc: "should successfully authenticate user with correct password",
req: &authn.Request{HTTPRequest: &http.Request{Header: map[string][]string{authorizationHeaderName: {encodeBasicAuth("user", "password")}}}},
expectedErr: nil,
expectedSignedInUser: &user.SignedInUser{UserID: 1, OrgID: 1, OrgRole: "Viewer"},
expectedIdentity: &authn.Identity{ID: "user:1", OrgID: 1, OrgRoles: map[int64]org.RoleType{1: "Viewer"}, IsGrafanaAdmin: boolPtr(false)},
},
{
desc: "should fail for incorrect password",
req: &authn.Request{HTTPRequest: &http.Request{Header: map[string][]string{authorizationHeaderName: {encodeBasicAuth("user", "wrong")}}}},
expectedErr: ErrBasicAuthCredentials,
},
{
desc: "should fail for empty password",
req: &authn.Request{HTTPRequest: &http.Request{Header: map[string][]string{authorizationHeaderName: {encodeBasicAuth("user", "")}}}},
expectedErr: ErrBasicAuthCredentials,
},
{
desc: "should if login is blocked by to many attempts",
req: &authn.Request{HTTPRequest: &http.Request{Header: map[string][]string{authorizationHeaderName: {encodeBasicAuth("user", "")}}}},
blockLogin: true,
expectedErr: ErrBasicAuthCredentials,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
hashed, _ := util.EncodePassword("password", "salt")
c := ProvideBasic(&usertest.FakeUserService{
ExpectedUser: &user.User{
Password: hashed,
Salt: "salt",
},
ExpectedSignedInUser: tt.expectedSignedInUser,
}, loginattempttest.FakeLoginAttemptService{
ExpectedValid: !tt.blockLogin,
})
identity, err := c.Authenticate(context.Background(), tt.req)
if tt.expectedErr != nil {
assert.ErrorIs(t, err, tt.expectedErr)
assert.Nil(t, identity)
} else {
assert.NoError(t, err)
assert.EqualValues(t, *tt.expectedIdentity, *identity)
}
})
}
}
func TestBasic_Test(t *testing.T) {
type TestCase struct {
desc string
req *authn.Request
expected bool
}
tests := []TestCase{
{
desc: "should succeed when authorization header is set with basic prefix",
req: &authn.Request{
HTTPRequest: &http.Request{
Header: map[string][]string{
authorizationHeaderName: {encodeBasicAuth("user", "password")},
},
},
},
expected: true,
},
{
desc: "should fail when no http request is passed",
req: &authn.Request{},
},
{
desc: "should fail when no http authorization header is set in http request",
req: &authn.Request{
HTTPRequest: &http.Request{Header: map[string][]string{}},
},
},
{
desc: "should fail when authorization header is set but without basic prefix",
req: &authn.Request{
HTTPRequest: &http.Request{Header: map[string][]string{authorizationHeaderName: {"something"}}},
},
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
c := ProvideBasic(usertest.NewUserServiceFake(), loginattempttest.FakeLoginAttemptService{})
assert.Equal(t, tt.expected, c.Test(context.Background(), tt.req))
})
}
}
+7
View File
@@ -0,0 +1,7 @@
package clients
const (
basicPrefix = "Basic "
bearerPrefix = "Bearer "
authorizationHeaderName = "Authorization"
)
@@ -396,6 +396,26 @@ func (h *ContextHandler) initContextWithAPIKey(reqContext *models.ReqContext) bo
}
func (h *ContextHandler) initContextWithBasicAuth(reqContext *models.ReqContext, orgID int64) bool {
if h.features.IsEnabled(featuremgmt.FlagAuthnService) {
identity, ok, err := h.authnService.Authenticate(reqContext.Req.Context(), authn.ClientBasic, &authn.Request{HTTPRequest: reqContext.Req})
if !ok {
return false
}
// include auth header in context
ctx := WithAuthHTTPHeader(reqContext.Req.Context(), "Authorization")
*reqContext.Req = *reqContext.Req.WithContext(ctx)
if err != nil {
writeErr(reqContext, err)
return true
}
reqContext.IsSignedIn = true
reqContext.SignedInUser = identity.SignedInUser()
return true
}
if !h.Cfg.BasicAuthEnabled {
return false
}
+9 -4
View File
@@ -7,7 +7,7 @@ import { useLocation } from 'react-router-dom';
import { GrafanaTheme2, NavModelItem, NavSection } from '@grafana/data';
import { config, locationSearchToObject, locationService, reportInteraction } from '@grafana/runtime';
import { Icon, useTheme2, CustomScrollbar } from '@grafana/ui';
import { useTheme2, CustomScrollbar, IconButton } from '@grafana/ui';
import { getKioskMode } from 'app/core/navigation/kiosk';
import { useSelector } from 'app/types';
@@ -90,8 +90,14 @@ export const NavBar = React.memo(() => {
}}
>
<FocusScope>
<div className={styles.mobileSidemenuLogo} onClick={() => setMenuOpen(!menuOpen)} key="hamburger">
<Icon name="bars" size="xl" />
<div className={styles.mobileSidemenuLogo} key="hamburger">
<IconButton
name="bars"
tooltip="Toggle menu"
tooltipPlacement="bottom"
size="xl"
onClick={() => setMenuOpen(!menuOpen)}
/>
</div>
<NavBarToggle
@@ -212,7 +218,6 @@ const getStyles = (theme: GrafanaTheme2) => ({
}),
mobileSidemenuLogo: css({
alignItems: 'center',
cursor: 'pointer',
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
@@ -1,83 +1,32 @@
import React, { PureComponent } from 'react';
import React from 'react';
import { useAsync } from 'react-use';
import { renderMarkdown } from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime';
import { LoadingPlaceholder } from '@grafana/ui';
interface Props {
plugin: {
name: string;
id: string;
};
type: string;
pluginId: string;
}
interface State {
isError: boolean;
isLoading: boolean;
help: string;
}
export function PluginHelp({ pluginId }: Props) {
const { value, loading, error } = useAsync(async () => {
return getBackendSrv().get(`/api/plugins/${pluginId}/markdown/query_help`);
}, []);
export class PluginHelp extends PureComponent<Props, State> {
state = {
isError: false,
isLoading: false,
help: '',
};
const renderedMarkdown = renderMarkdown(value);
componentDidMount(): void {
this.loadHelp();
if (loading) {
return <LoadingPlaceholder text="Loading help..." />;
}
constructPlaceholderInfo() {
return 'No plugin help or readme markdown file was found';
if (error) {
return <h3>An error occurred when loading help.</h3>;
}
loadHelp = () => {
const { plugin, type } = this.props;
this.setState({ isLoading: true });
getBackendSrv()
.get(`/api/plugins/${plugin.id}/markdown/${type}`)
.then((response: string) => {
const helpHtml = renderMarkdown(response);
if (response === '' && type === 'help') {
this.setState({
isError: false,
isLoading: false,
help: this.constructPlaceholderInfo(),
});
} else {
this.setState({
isError: false,
isLoading: false,
help: helpHtml,
});
}
})
.catch(() => {
this.setState({
isError: true,
isLoading: false,
});
});
};
render() {
const { type } = this.props;
const { isError, isLoading, help } = this.state;
if (isLoading) {
return <h2>Loading help...</h2>;
}
if (isError) {
return <h3>&apos;Error occurred when loading help&apos;</h3>;
}
if (type === 'panel_help' && help === '') {
}
return <div className="markdown-html" dangerouslySetInnerHTML={{ __html: help }} />;
if (value === '') {
return <h3>No query help could be found.</h3>;
}
return <div className="markdown-html" dangerouslySetInnerHTML={{ __html: renderedMarkdown }} />;
}
@@ -2,17 +2,33 @@ import { act, render, screen } from '@testing-library/react';
import React from 'react';
import { Provider } from 'react-redux';
import { Router } from 'react-router-dom';
import { byRole } from 'testing-library-selector';
import { DataSourceJsonData, PluginMeta } from '@grafana/data';
import { locationService } from '@grafana/runtime';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import { contextSrv } from 'app/core/services/context_srv';
import { configureStore } from 'app/store/configureStore';
import { AccessControlAction } from 'app/types';
import { CombinedRule } from 'app/types/unified-alerting';
import { GrafanaAlertStateDecision } from 'app/types/unified-alerting-dto';
import { RuleViewer } from './RuleViewer';
import { useCombinedRule } from './hooks/useCombinedRule';
import { GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
import { useIsRuleEditable } from './hooks/useIsRuleEditable';
import { getCloudRule, getGrafanaRule } from './mocks';
const mockGrafanaRule = getGrafanaRule({ name: 'Test alert' });
const mockCloudRule = getCloudRule({ name: 'cloud test alert' });
const mockRoute: GrafanaRouteComponentProps<{ id?: string; sourceName?: string }> = {
route: {
path: '/',
component: RuleViewer,
},
queryParams: { returnTo: '/alerting/list' },
match: { params: { id: 'test1', sourceName: 'grafana' }, isExact: false, url: 'asdf', path: '' },
history: locationService.getHistory(),
location: { pathname: '', hash: '', search: '', state: '' },
staticContext: {},
};
jest.mock('./hooks/useCombinedRule');
jest.mock('@grafana/runtime', () => ({
@@ -40,6 +56,20 @@ const renderRuleViewer = () => {
);
});
};
const ui = {
actionButtons: {
edit: byRole('link', { name: /edit/i }),
delete: byRole('button', { name: /delete/i }),
silence: byRole('link', { name: 'Silence' }),
},
};
jest.mock('./hooks/useIsRuleEditable');
const mocks = {
useIsRuleEditable: jest.mocked(useIsRuleEditable),
};
describe('RuleViewer', () => {
let mockCombinedRule: jest.MockedFn<typeof useCombinedRule>;
@@ -59,6 +89,7 @@ describe('RuleViewer', () => {
requestId: 'A',
error: undefined,
});
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isEditable: false });
await renderRuleViewer();
expect(screen.getByText(/view rule/i)).toBeInTheDocument();
@@ -73,82 +104,142 @@ describe('RuleViewer', () => {
requestId: 'A',
error: undefined,
});
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isEditable: false });
await renderRuleViewer();
expect(screen.getByText(/view rule/i)).toBeInTheDocument();
expect(screen.getByText(/cloud test alert/i)).toBeInTheDocument();
});
});
const mockGrafanaRule = {
name: 'Test alert',
query: 'up',
labels: {},
annotations: {},
group: {
name: 'Prom up alert',
rules: [],
},
namespace: {
rulesSource: GRAFANA_RULES_SOURCE_NAME,
name: 'Alerts',
groups: [],
},
rulerRule: {
for: '',
annotations: {},
labels: {},
grafana_alert: {
condition: 'B',
exec_err_state: GrafanaAlertStateDecision.Alerting,
namespace_id: 11,
namespace_uid: 'namespaceuid123',
no_data_state: GrafanaAlertStateDecision.NoData,
title: 'Test alert',
uid: 'asdf23',
data: [],
},
},
};
describe('RuleDetails RBAC', () => {
describe('Grafana rules action buttons in details', () => {
let mockCombinedRule: jest.MockedFn<typeof useCombinedRule>;
const mockCloudRule = {
name: 'Cloud test alert',
labels: {},
query: 'up == 0',
annotations: {},
group: {
name: 'test',
rules: [],
},
promRule: {
health: 'ok',
name: 'cloud up alert',
query: 'up == 0',
type: 'alerting',
},
namespace: {
name: 'prom test alerts',
groups: [],
rulesSource: {
name: 'prom test',
type: 'prometheus',
uid: 'asdf23',
id: 1,
meta: {} as PluginMeta,
jsonData: {} as DataSourceJsonData,
access: 'proxy',
readOnly: false,
},
},
};
beforeEach(() => {
mockCombinedRule = jest.mocked(useCombinedRule);
});
const mockRoute: GrafanaRouteComponentProps<{ id?: string; sourceName?: string }> = {
route: {
path: '/',
component: RuleViewer,
},
queryParams: { returnTo: '/alerting/list' },
match: { params: { id: 'test1', sourceName: 'grafana' }, isExact: false, url: 'asdf', path: '' },
history: locationService.getHistory(),
location: { pathname: '', hash: '', search: '', state: '' },
staticContext: {},
};
afterEach(() => {
mockCombinedRule.mockReset();
});
it('Should render Edit button for users with the update permission', async () => {
// Arrange
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isEditable: true });
mockCombinedRule.mockReturnValue({
result: mockGrafanaRule as CombinedRule,
loading: false,
dispatched: true,
requestId: 'A',
error: undefined,
});
// Act
await renderRuleViewer();
// Assert
expect(ui.actionButtons.edit.get()).toBeInTheDocument();
});
it('Should render Delete button for users with the delete permission', async () => {
// Arrange
mockCombinedRule.mockReturnValue({
result: mockGrafanaRule as CombinedRule,
loading: false,
dispatched: true,
requestId: 'A',
error: undefined,
});
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isRemovable: true });
// Act
await renderRuleViewer();
// Assert
expect(ui.actionButtons.delete.get()).toBeInTheDocument();
});
it('Should not render Silence button for users wihout the instance create permission', async () => {
// Arrange
mockCombinedRule.mockReturnValue({
result: mockGrafanaRule as CombinedRule,
loading: false,
dispatched: true,
requestId: 'A',
error: undefined,
});
jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(false);
// Act
await renderRuleViewer();
// Assert
expect(ui.actionButtons.silence.query()).not.toBeInTheDocument();
});
it('Should render Silence button for users with the instance create permissions', async () => {
// Arrange
mockCombinedRule.mockReturnValue({
result: mockGrafanaRule as CombinedRule,
loading: false,
dispatched: true,
requestId: 'A',
error: undefined,
});
jest
.spyOn(contextSrv, 'hasPermission')
.mockImplementation((action) => action === AccessControlAction.AlertingInstanceCreate);
// Act
await renderRuleViewer();
// Assert
expect(ui.actionButtons.silence.query()).toBeInTheDocument();
});
});
describe('Cloud rules action buttons', () => {
let mockCombinedRule: jest.MockedFn<typeof useCombinedRule>;
beforeEach(() => {
mockCombinedRule = jest.mocked(useCombinedRule);
});
afterEach(() => {
mockCombinedRule.mockReset();
});
it('Should render edit button for users with the update permission', async () => {
// Arrange
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isEditable: true });
mockCombinedRule.mockReturnValue({
result: mockCloudRule as CombinedRule,
loading: false,
dispatched: true,
requestId: 'A',
error: undefined,
});
// Act
await renderRuleViewer();
// Assert
expect(ui.actionButtons.edit.query()).toBeInTheDocument();
});
it('Should render Delete button for users with the delete permission', async () => {
// Arrange
mockCombinedRule.mockReturnValue({
result: mockCloudRule as CombinedRule,
loading: false,
dispatched: true,
requestId: 'A',
error: undefined,
});
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isRemovable: true });
// Act
await renderRuleViewer();
// Assert
expect(ui.actionButtons.delete.query()).toBeInTheDocument();
});
});
});
@@ -170,7 +170,7 @@ export function RuleViewer({ match }: RuleViewerProps) {
<Icon name="bell" size="lg" /> {rule.name}
</h4>
<RuleState rule={rule} isCreating={false} isDeleting={false} />
<RuleDetailsActionButtons rule={rule} rulesSource={rulesSource} />
<RuleDetailsActionButtons rule={rule} rulesSource={rulesSource} isViewMode={true} />
</div>
<div className={styles.details}>
<div className={styles.leftSide}>
@@ -9,24 +9,57 @@ import { configureStore } from 'app/store/configureStore';
import { AccessControlAction } from 'app/types';
import { CombinedRule } from 'app/types/unified-alerting';
import { mockCombinedRule } from '../../mocks';
import { useIsRuleEditable } from '../../hooks/useIsRuleEditable';
import { getCloudRule, getGrafanaRule } from '../../mocks';
import { RuleDetails } from './RuleDetails';
jest.mock('../../hooks/useIsRuleEditable');
const mocks = {
useIsRuleEditable: jest.mocked(useIsRuleEditable),
};
const ui = {
actionButtons: {
edit: byRole('link', { name: 'Edit' }),
delete: byRole('button', { name: 'Delete' }),
edit: byRole('link', { name: /edit/i }),
delete: byRole('button', { name: /delete/i }),
silence: byRole('link', { name: 'Silence' }),
},
};
jest.spyOn(contextSrv, 'accessControlEnabled').mockReturnValue(true);
beforeEach(() => {
jest.clearAllMocks();
});
describe('RuleDetails RBAC', () => {
describe('Grafana rules action buttons in details', () => {
const grafanaRule = getGrafanaRule({ name: 'Grafana' });
it('Should not render Edit button for users with the update permission', () => {
// Arrange
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isEditable: true });
// Act
renderRuleDetails(grafanaRule);
// Assert
expect(ui.actionButtons.edit.query()).not.toBeInTheDocument();
});
it('Should not render Delete button for users with the delete permission', () => {
// Arrange
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isRemovable: true });
// Act
renderRuleDetails(grafanaRule);
// Assert
expect(ui.actionButtons.delete.query()).not.toBeInTheDocument();
});
it('Should not render Silence button for users wihout the instance create permission', () => {
// Arrange
jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(false);
@@ -51,6 +84,31 @@ describe('RuleDetails RBAC', () => {
expect(ui.actionButtons.silence.query()).toBeInTheDocument();
});
});
describe('Cloud rules action buttons', () => {
const cloudRule = getCloudRule({ name: 'Cloud' });
it('Should not render Edit button for users with the update permission', () => {
// Arrange
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isEditable: true });
// Act
renderRuleDetails(cloudRule);
// Assert
expect(ui.actionButtons.edit.query()).not.toBeInTheDocument();
});
it('Should not render Delete button for users with the delete permission', () => {
// Arrange
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isRemovable: true });
// Act
renderRuleDetails(cloudRule);
// Assert
expect(ui.actionButtons.delete.query()).not.toBeInTheDocument();
});
});
});
function renderRuleDetails(rule: CombinedRule) {
@@ -64,14 +122,3 @@ function renderRuleDetails(rule: CombinedRule) {
</Provider>
);
}
function getGrafanaRule(override?: Partial<CombinedRule>) {
return mockCombinedRule({
namespace: {
groups: [],
name: 'Grafana',
rulesSource: 'grafana',
},
...override,
});
}
@@ -34,7 +34,7 @@ export const RuleDetails: FC<Props> = ({ rule }) => {
return (
<div>
<RuleDetailsActionButtons rule={rule} rulesSource={rulesSource} />
<RuleDetailsActionButtons rule={rule} rulesSource={rulesSource} isViewMode={false} />
<div className={styles.wrapper}>
<div className={styles.leftSide}>
{<EvaluationBehaviorSummary rule={rule} />}
@@ -1,29 +1,41 @@
import { css } from '@emotion/css';
import React, { FC, Fragment } from 'react';
import React, { FC, Fragment, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { GrafanaTheme2, textUtil } from '@grafana/data';
import { Button, HorizontalGroup, LinkButton, useStyles2 } from '@grafana/ui';
import { GrafanaTheme2, textUtil, urlUtil } from '@grafana/data';
import { config } from '@grafana/runtime';
import { Button, ClipboardButton, ConfirmModal, HorizontalGroup, LinkButton, useStyles2 } from '@grafana/ui';
import { useAppNotification } from 'app/core/copy/appNotification';
import { contextSrv } from 'app/core/services/context_srv';
import { AccessControlAction } from 'app/types';
import { AccessControlAction, useDispatch } from 'app/types';
import { CombinedRule, RulesSource } from 'app/types/unified-alerting';
import { useIsRuleEditable } from '../../hooks/useIsRuleEditable';
import { useStateHistoryModal } from '../../hooks/useStateHistoryModal';
import { deleteRuleAction } from '../../state/actions';
import { getAlertmanagerByUid } from '../../utils/alertmanager';
import { Annotation } from '../../utils/constants';
import { isCloudRulesSource, isGrafanaRulesSource } from '../../utils/datasource';
import { getRulesSourceName, isCloudRulesSource, isGrafanaRulesSource } from '../../utils/datasource';
import { createExploreLink, makeRuleBasedSilenceLink } from '../../utils/misc';
import * as ruleId from '../../utils/rule-id';
import { isFederatedRuleGroup, isGrafanaRulerRule } from '../../utils/rules';
interface Props {
rule: CombinedRule;
rulesSource: RulesSource;
isViewMode: boolean;
}
export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource }) => {
export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource, isViewMode }) => {
const style = useStyles2(getStyles);
const { group } = rule;
const { namespace, group, rulerRule } = rule;
const alertId = isGrafanaRulerRule(rule.rulerRule) ? rule.rulerRule.grafana_alert.id ?? '' : '';
const { StateHistoryModal, showStateHistoryModal } = useStateHistoryModal(alertId);
const dispatch = useDispatch();
const location = useLocation();
const notifyApp = useAppNotification();
const [ruleToDelete, setRuleToDelete] = useState<CombinedRule>();
const alertmanagerSourceName = isGrafanaRulesSource(rulesSource)
? rulesSource
@@ -32,9 +44,39 @@ export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource }) => {
const hasExplorePermission = contextSrv.hasPermission(AccessControlAction.DataSourcesExplore);
const buttons: JSX.Element[] = [];
const rightButtons: JSX.Element[] = [];
const deleteRule = () => {
if (ruleToDelete && ruleToDelete.rulerRule) {
const identifier = ruleId.fromRulerRule(
getRulesSourceName(ruleToDelete.namespace.rulesSource),
ruleToDelete.namespace.name,
ruleToDelete.group.name,
ruleToDelete.rulerRule
);
dispatch(deleteRuleAction(identifier, { navigateTo: isViewMode ? '/alerting/list' : undefined }));
setRuleToDelete(undefined);
}
};
const buildShareUrl = () => {
if (isCloudRulesSource(rulesSource)) {
const { appUrl, appSubUrl } = config;
const baseUrl = appSubUrl !== '' ? `${appUrl}${appSubUrl}/` : config.appUrl;
const ruleUrl = `${encodeURIComponent(rulesSource.name)}/${encodeURIComponent(rule.name)}`;
return `${baseUrl}alerting/${ruleUrl}/find`;
}
return window.location.href.split('?')[0];
};
const isFederated = isFederatedRuleGroup(group);
const rulesSourceName = getRulesSourceName(rulesSource);
const isProvisioned = isGrafanaRulerRule(rule.rulerRule) && Boolean(rule.rulerRule.grafana_alert.provenance);
const { isEditable, isRemovable } = useIsRuleEditable(rulesSourceName, rulerRule);
const returnTo = location.pathname + location.search;
// explore does not support grafana rule queries atm
// neither do "federated rules"
if (isCloudRulesSource(rulesSource) && hasExplorePermission && !isFederated) {
@@ -128,14 +170,77 @@ export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource }) => {
);
}
if (buttons.length) {
return (
<div className={style.wrapper}>
<HorizontalGroup width="auto">{buttons.length ? buttons : <div />}</HorizontalGroup>
</div>
);
if (isViewMode) {
if (isEditable && rulerRule && !isFederated && !isProvisioned) {
const sourceName = getRulesSourceName(rulesSource);
const identifier = ruleId.fromRulerRule(sourceName, namespace.name, group.name, rulerRule);
const editURL = urlUtil.renderUrl(
`${config.appSubUrl}/alerting/${encodeURIComponent(ruleId.stringifyIdentifier(identifier))}/edit`,
{
returnTo,
}
);
rightButtons.push(
<ClipboardButton
key="copy"
icon="copy"
onClipboardError={(copiedText) => {
notifyApp.error('Error while copying URL', copiedText);
}}
className={style.button}
size="sm"
getText={buildShareUrl}
>
Copy link to rule
</ClipboardButton>
);
rightButtons.push(
<LinkButton className={style.button} size="xs" key="edit" variant="secondary" icon="pen" href={editURL}>
Edit
</LinkButton>
);
}
if (isRemovable && rulerRule && !isFederated && !isProvisioned) {
rightButtons.push(
<Button
className={style.button}
size="xs"
type="button"
key="delete"
variant="secondary"
icon="trash-alt"
onClick={() => setRuleToDelete(rule)}
>
Delete
</Button>
);
}
}
if (buttons.length || rightButtons.length) {
return (
<>
<div className={style.wrapper}>
<HorizontalGroup width="auto">{buttons.length ? buttons : <div />}</HorizontalGroup>
<HorizontalGroup width="auto">{rightButtons.length ? rightButtons : <div />}</HorizontalGroup>
</div>
{!!ruleToDelete && (
<ConfirmModal
isOpen={true}
title="Delete rule"
body="Deleting this rule will permanently remove it from your alert rule list. Are you sure you want to delete this rule?"
confirmText="Yes, delete"
icon="exclamation-triangle"
onConfirm={deleteRule}
onDismiss={() => setRuleToDelete(undefined)}
/>
)}
</>
);
}
return null;
};
@@ -150,7 +255,6 @@ export const getStyles = (theme: GrafanaTheme2) => ({
`,
button: css`
height: 24px;
margin-top: ${theme.spacing(1)};
font-size: ${theme.typography.size.sm};
`,
});
@@ -9,7 +9,7 @@ import { configureStore } from 'app/store/configureStore';
import { CombinedRule } from 'app/types/unified-alerting';
import { useIsRuleEditable } from '../../hooks/useIsRuleEditable';
import { mockCombinedRule, mockDataSource, mockPromAlertingRule, mockRulerAlertingRule } from '../../mocks';
import { getCloudRule, getGrafanaRule } from '../../mocks';
import { RulesTable } from './RulesTable';
@@ -41,30 +41,6 @@ function renderRulesTable(rule: CombinedRule) {
);
}
function getGrafanaRule(override?: Partial<CombinedRule>) {
return mockCombinedRule({
namespace: {
groups: [],
name: 'Grafana',
rulesSource: 'grafana',
},
...override,
});
}
function getCloudRule(override?: Partial<CombinedRule>) {
return mockCombinedRule({
namespace: {
groups: [],
name: 'Cortex',
rulesSource: mockDataSource(),
},
promRule: mockPromAlertingRule(),
rulerRule: mockRulerAlertingRule(),
...override,
});
}
describe('RulesTable RBAC', () => {
describe('Grafana rules action buttons', () => {
const grafanaRule = getGrafanaRule({ name: 'Grafana' });
@@ -516,3 +516,26 @@ export function mockStore(recipe: (state: StoreState) => void) {
return configureStore(produce(defaultState, recipe));
}
export function getGrafanaRule(override?: Partial<CombinedRule>) {
return mockCombinedRule({
namespace: {
groups: [],
name: 'Grafana',
rulesSource: 'grafana',
},
...override,
});
}
export function getCloudRule(override?: Partial<CombinedRule>) {
return mockCombinedRule({
namespace: {
groups: [],
name: 'Cortex',
rulesSource: mockDataSource(),
},
promRule: mockPromAlertingRule(),
rulerRule: mockRulerAlertingRule(),
...override,
});
}
+1 -1
View File
@@ -15,5 +15,5 @@ export const ROUTES = {
// Connect Data
ConnectData: `/${ROUTE_BASE_ID}/connect-data`,
DataSourcesDetails: `/${ROUTE_BASE_ID}/connect-data/datasources/:id`,
DataSourcesDetails: `/${ROUTE_BASE_ID}/datasources/:id`,
} as const;
@@ -466,7 +466,7 @@ export class QueryGroup extends PureComponent<Props, State> {
{this.renderAddQueryRow(dsSettings, styles)}
{isHelpOpen && (
<Modal title="Data source help" isOpen={true} onDismiss={this.onCloseHelp}>
<PluginHelp plugin={dsSettings.meta} type="query_help" />
<PluginHelp pluginId={dsSettings.meta.id} />
</Modal>
)}
</>
@@ -1,14 +1,11 @@
import { debounce } from 'lodash';
import React, { useEffect, useMemo, useState } from 'react';
import { Icon, Input, useStyles2 } from '@grafana/ui';
import getStyles from './components/styles';
import { Icon, Input } from '@grafana/ui';
// TODO: consider moving search into grafana/ui, this is mostly the same as that in azure monitor
const Search = ({ searchFn, searchPhrase }: { searchPhrase: string; searchFn: (searchPhrase: string) => void }) => {
const [searchFilter, setSearchFilter] = useState(searchPhrase);
const styles = useStyles2(getStyles);
const debouncedSearch = useMemo(() => debounce(searchFn, 600), [searchFn]);
@@ -21,8 +18,6 @@ const Search = ({ searchFn, searchPhrase }: { searchPhrase: string; searchFn: (s
return (
<Input
className={styles.search}
width={64}
aria-label="log group search"
prefix={<Icon name="search" />}
value={searchFilter}
@@ -177,4 +177,44 @@ describe('CrossAccountLogsQueryField', () => {
},
]);
});
const labelText =
'Only the first 50 results can be shown. If you do not see an expected log group, try narrowing down your search.';
it('should not display max result info label in case less than 50 logs groups are being displayed', async () => {
const defer = new Deferred();
const fetchLogGroups = jest.fn(async () => {
await Promise.all([defer.promise]);
return [];
});
render(<CrossAccountLogsQueryField {...defaultProps} fetchLogGroups={fetchLogGroups} />);
await userEvent.click(screen.getByText('Select Log Groups'));
expect(screen.queryByText(labelText)).not.toBeInTheDocument();
defer.resolve();
await waitFor(() => expect(screen.queryByText(labelText)).not.toBeInTheDocument());
});
it('should display max result info label in case 50 or more logs groups are being displayed', async () => {
const defer = new Deferred();
const fetchLogGroups = jest.fn(async () => {
await Promise.all([defer.promise]);
return Array(50).map((i) => ({
value: `logGroup${i}`,
text: `logGroup${i}`,
label: `logGroup${i}`,
}));
});
render(<CrossAccountLogsQueryField {...defaultProps} fetchLogGroups={fetchLogGroups} />);
await userEvent.click(screen.getByText('Select Log Groups'));
expect(screen.queryByText(labelText)).not.toBeInTheDocument();
defer.resolve();
await waitFor(() => expect(screen.getByText(labelText)).toBeInTheDocument());
});
it('should display log groups counter label', async () => {
render(<CrossAccountLogsQueryField {...defaultProps} selectedLogGroups={[]} />);
await userEvent.click(screen.getByText('Select Log Groups'));
await waitFor(() => expect(screen.getByText('0 log groups selected')).toBeInTheDocument());
await userEvent.click(screen.getByLabelText('logGroup2'));
await waitFor(() => expect(screen.getByText('1 log group selected')).toBeInTheDocument());
});
});
@@ -2,13 +2,14 @@ import React, { useMemo, useState } from 'react';
import { SelectableValue } from '@grafana/data';
import { EditorField, Space } from '@grafana/experimental';
import { Button, Checkbox, IconButton, LoadingPlaceholder, Modal, useStyles2 } from '@grafana/ui';
import { Button, Checkbox, Icon, Label, LoadingPlaceholder, Modal, useStyles2 } from '@grafana/ui';
import Search from '../Search';
import { SelectableResourceValue } from '../api';
import { DescribeLogGroupsRequest } from '../types';
import { Account, ALL_ACCOUNTS_OPTION } from './Account';
import { SelectedLogsGroups } from './SelectedLogsGroups';
import getStyles from './styles';
type CrossAccountLogsQueryProps = {
@@ -81,15 +82,17 @@ export const CrossAccountLogsQueryField = (props: CrossAccountLogsQueryProps) =>
<>
<Modal className={styles.modal} title="Select Log Groups" isOpen={isModalOpen} onDismiss={toggleModal}>
<div className={styles.logGroupSelectionArea}>
<EditorField label="Log Group Name">
<Search
searchFn={(phrase) => {
searchFn(phrase, searchAccountId);
setSearchPhrase(phrase);
}}
searchPhrase={searchPhrase}
/>
</EditorField>
<div className={styles.searchField}>
<EditorField label="Log Group Name">
<Search
searchFn={(phrase) => {
searchFn(phrase, searchAccountId);
setSearchPhrase(phrase);
}}
searchPhrase={searchPhrase}
/>
</EditorField>
</div>
<Account
onChange={(accountId?: string) => {
@@ -102,6 +105,16 @@ export const CrossAccountLogsQueryField = (props: CrossAccountLogsQueryProps) =>
</div>
<Space layout="block" v={2} />
<div>
{!isLoading && selectableLogGroups.length >= 25 && (
<>
<Label className={styles.limitLabel}>
<Icon name="info-circle"></Icon>
Only the first 50 results can be shown. If you do not see an expected log group, try narrowing down your
search.
</Label>
<Space layout="block" v={1} />
</>
)}
<div className={styles.tableScroller}>
<table className={styles.table}>
<thead>
@@ -149,6 +162,10 @@ export const CrossAccountLogsQueryField = (props: CrossAccountLogsQueryProps) =>
</div>
</div>
<Space layout="block" v={2} />
<Label className={styles.logGroupCountLabel}>
{selectedLogGroups.length} log group{selectedLogGroups.length !== 1 && 's'} selected
</Label>
<Space layout="block" v={1.5} />
<div>
<Button onClick={handleApply} type="button" className={styles.addBtn}>
Add log groups
@@ -165,19 +182,7 @@ export const CrossAccountLogsQueryField = (props: CrossAccountLogsQueryProps) =>
</Button>
</div>
<div className={styles.selectedLogGroupsContainer}>
{props.selectedLogGroups.map((lg) => (
<div key={lg.value} className={styles.selectedLogGroup}>
{lg.label}
<IconButton
size="sm"
name="times"
className={styles.removeButton}
onClick={() => props.onChange(props.selectedLogGroups.filter((slg) => slg.value !== lg.value))}
/>
</div>
))}
</div>
<SelectedLogsGroups selectedLogGroups={props.selectedLogGroups} onChange={props.onChange}></SelectedLogsGroups>
</>
);
};
@@ -0,0 +1,93 @@
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { SelectedLogsGroups } from './SelectedLogsGroups';
const defaultProps = {
selectedLogGroups: [
{
value: 'aws/lambda/lambda-name1',
label: 'aws/lambda/lambda-name1',
text: 'aws/lambda/lambda-name1',
},
{
value: 'aws/lambda/lambda-name2',
label: 'aws/lambda/lambda-name2',
text: 'aws/lambda/lambda-name2',
},
],
onChange: jest.fn(),
};
describe('SelectedLogsGroups', () => {
afterEach(() => {
jest.resetAllMocks();
});
describe("'Show more' button", () => {
it('should not be displayed in case 0 logs have been selected', async () => {
render(<SelectedLogsGroups {...defaultProps} selectedLogGroups={[]} />);
await waitFor(() => expect(screen.queryByText('Show all')).not.toBeInTheDocument());
});
it('should not be displayed in case logs group have been selected but theyre less than 10', async () => {
render(<SelectedLogsGroups {...defaultProps} />);
await waitFor(() => expect(screen.queryByText('Show all')).not.toBeInTheDocument());
});
it('should be displayed in case more than 10 log groups have been selected', async () => {
const selectedLogGroups = Array(12).map((i) => ({
value: `logGroup${i}`,
text: `logGroup${i}`,
label: `logGroup${i}`,
}));
render(<SelectedLogsGroups {...defaultProps} selectedLogGroups={selectedLogGroups} />);
await waitFor(() => expect(screen.getByText('Show all')).toBeInTheDocument());
});
});
describe("'Clear selection' button", () => {
it('should not be displayed in case 0 logs have been selected', async () => {
render(<SelectedLogsGroups {...defaultProps} selectedLogGroups={[]} />);
await waitFor(() => expect(screen.queryByText('Clear selection')).not.toBeInTheDocument());
});
it('should be displayed in case at least one log group have been selected', async () => {
const selectedLogGroups = Array(11).map((i) => ({
value: `logGroup${i}`,
text: `logGroup${i}`,
label: `logGroup${i}`,
}));
render(<SelectedLogsGroups {...defaultProps} selectedLogGroups={selectedLogGroups} />);
await waitFor(() => expect(screen.getByText('Clear selection')).toBeInTheDocument());
});
it('should display confirm dialog before clearing all selections', async () => {
const selectedLogGroups = Array(11).map((i) => ({
value: `logGroup${i}`,
text: `logGroup${i}`,
label: `logGroup${i}`,
}));
render(<SelectedLogsGroups {...defaultProps} selectedLogGroups={selectedLogGroups} />);
await waitFor(() => userEvent.click(screen.getByText('Clear selection')));
await waitFor(() =>
expect(screen.getByText('Are you sure you want to clear all log groups?')).toBeInTheDocument()
);
await waitFor(() => userEvent.click(screen.getByLabelText('Confirm Modal Danger Button')));
expect(defaultProps.onChange).toHaveBeenCalledWith([]);
});
});
describe("'Clear selection' button", () => {
it('should not be displayed in case 0 logs have been selected', async () => {
render(<SelectedLogsGroups {...defaultProps} selectedLogGroups={[]} />);
await waitFor(() => expect(screen.queryByText('Clear selection')).not.toBeInTheDocument());
});
it('should be displayed in case at least one log group have been selected', async () => {
const selectedLogGroups = Array(11).map((i) => ({
value: `logGroup${i}`,
text: `logGroup${i}`,
label: `logGroup${i}`,
}));
render(<SelectedLogsGroups {...defaultProps} selectedLogGroups={selectedLogGroups} />);
await waitFor(() => expect(screen.getByText('Clear selection')).toBeInTheDocument());
});
});
});
@@ -0,0 +1,84 @@
import React, { useEffect, useState } from 'react';
import { Button, ConfirmModal, useStyles2 } from '@grafana/ui';
import { SelectableResourceValue } from '../api';
import getStyles from './styles';
type CrossAccountLogsQueryProps = {
selectedLogGroups: SelectableResourceValue[];
onChange: (selectedLogGroups: SelectableResourceValue[]) => void;
};
const MAX_VISIBLE_LOG_GROUPS = 6;
export const SelectedLogsGroups = ({ selectedLogGroups, onChange }: CrossAccountLogsQueryProps) => {
const styles = useStyles2(getStyles);
const [showConfirm, setShowConfirm] = useState(false);
const [visibleSelectecLogGroups, setVisibleSelectecLogGroups] = useState(
selectedLogGroups.slice(0, MAX_VISIBLE_LOG_GROUPS)
);
useEffect(() => {
setVisibleSelectecLogGroups(selectedLogGroups.slice(0, MAX_VISIBLE_LOG_GROUPS));
}, [selectedLogGroups]);
return (
<>
<div className={styles.selectedLogGroupsContainer}>
{visibleSelectecLogGroups.map((lg) => (
<Button
key={lg.value}
size="sm"
variant="secondary"
icon="times"
className={styles.removeButton}
onClick={() => {
onChange(selectedLogGroups.filter((slg) => slg.value !== lg.value));
}}
>
{lg.label}
</Button>
))}
{visibleSelectecLogGroups.length !== selectedLogGroups.length && (
<Button
size="sm"
variant="secondary"
icon="plus"
fill="outline"
className={styles.removeButton}
onClick={() => setVisibleSelectecLogGroups(selectedLogGroups)}
>
Show all
</Button>
)}
{selectedLogGroups.length > 0 && (
<Button
size="sm"
variant="secondary"
icon="times"
fill="outline"
className={styles.removeButton}
onClick={() => setShowConfirm(true)}
>
Clear selection
</Button>
)}
</div>
<ConfirmModal
isOpen={showConfirm}
title="Clear Log Group Selection"
body="Are you sure you want to clear all log groups?"
confirmText="Yes"
dismissText="No"
icon="exclamation-triangle"
onConfirm={() => {
setShowConfirm(false);
onChange([]);
}}
onDismiss={() => setShowConfirm(false)}
/>
</>
);
};
@@ -10,12 +10,31 @@ const getStyles = (theme: GrafanaTheme2) => ({
selectedLogGroupsContainer: css({
marginLeft: theme.spacing(0.5),
marginBottom: theme.spacing(0.5),
display: 'flex',
flexFlow: 'wrap',
gap: theme.spacing(1),
button: {
margin: 'unset',
},
}),
limitLabel: css({
color: theme.colors.text.secondary,
textAlign: 'center',
maxWidth: 'none',
svg: {
marginRight: theme.spacing(0.5),
},
}),
logGroupCountLabel: css({
color: theme.colors.text.secondary,
maxWidth: 'none',
}),
tableScroller: css({
maxHeight: '50vh',
maxHeight: '40vh',
overflow: 'auto',
}),
@@ -61,24 +80,16 @@ const getStyles = (theme: GrafanaTheme2) => ({
display: 'flex',
}),
searchField: css({
width: '100%',
marginRight: theme.spacing(1),
}),
resultLimit: css({
margin: '4px 0',
fontStyle: 'italic',
}),
selectedLogGroup: css({
background: theme.colors.background.secondary,
borderRadius: theme.shape.borderRadius(),
margin: theme.spacing(0, 1, 1, 0),
padding: theme.spacing(0.5, 0, 0.5, 1),
color: theme.colors.text.primary,
fontSize: theme.typography.size.sm,
}),
search: css({
marginRight: '10px',
}),
removeButton: css({
verticalAlign: 'middle',
marginLeft: theme.spacing(0.5),
@@ -0,0 +1,66 @@
import { AbstractLabelOperator, DataSourceInstanceSettings, PluginMetaInfo, PluginType } from '@grafana/data';
import { PhlareDataSource } from './datasource';
describe('Phlare data source', () => {
let ds: PhlareDataSource;
beforeEach(() => {
ds = new PhlareDataSource(defaultSettings);
});
describe('importing queries', () => {
it('keeps all labels and values', async () => {
const queries = await ds.importFromAbstractQueries([
{
refId: 'A',
labelMatchers: [
{ name: 'foo', operator: AbstractLabelOperator.Equal, value: 'bar' },
{ name: 'foo2', operator: AbstractLabelOperator.Equal, value: 'bar2' },
],
},
]);
expect(queries[0].labelSelector).toBe('{foo="bar", foo2="bar2"}');
});
});
describe('exporting queries', () => {
it('keeps all labels and values', async () => {
const queries = await ds.exportToAbstractQueries([
{
refId: 'A',
labelSelector: '{foo="bar", foo2="bar2"}',
queryType: 'both',
profileTypeId: '',
groupBy: [''],
},
]);
expect(queries).toMatchObject([
{
refId: 'A',
labelMatchers: [
{ name: 'foo', operator: AbstractLabelOperator.Equal, value: 'bar' },
{ name: 'foo2', operator: AbstractLabelOperator.Equal, value: 'bar2' },
],
},
]);
});
});
});
const defaultSettings: DataSourceInstanceSettings = {
id: 0,
uid: 'phlare',
type: 'profiling',
name: 'phlare',
access: 'proxy',
meta: {
id: 'phlare',
name: 'phlare',
type: PluginType.datasource,
info: {} as PluginMetaInfo,
module: '',
baseUrl: '',
},
jsonData: {},
readOnly: false,
};
@@ -1,8 +1,11 @@
import Prism, { Grammar } from 'prismjs';
import { Observable, of } from 'rxjs';
import { DataQueryRequest, DataQueryResponse, DataSourceInstanceSettings } from '@grafana/data';
import { AbstractQuery, DataQueryRequest, DataQueryResponse, DataSourceInstanceSettings } from '@grafana/data';
import { DataSourceWithBackend } from '@grafana/runtime';
import { extractLabelMatchers, toPromLikeExpr } from '../prometheus/language_utils';
import { normalizeQuery } from './QueryEditor/QueryEditor';
import { PhlareDataSourceOptions, Query, ProfileTypeMessage, SeriesMessage } from './types';
@@ -45,4 +48,58 @@ export class PhlareDataSource extends DataSourceWithBackend<Query, PhlareDataSou
async getLabelNames(): Promise<string[]> {
return await super.getResource('labelNames');
}
async importFromAbstractQueries(abstractQueries: AbstractQuery[]): Promise<Query[]> {
return abstractQueries.map((abstractQuery) => this.importFromAbstractQuery(abstractQuery));
}
importFromAbstractQuery(labelBasedQuery: AbstractQuery): Query {
return {
refId: labelBasedQuery.refId,
labelSelector: toPromLikeExpr(labelBasedQuery),
queryType: 'both',
profileTypeId: '',
groupBy: [''],
};
}
async exportToAbstractQueries(queries: Query[]): Promise<AbstractQuery[]> {
return queries.map((query) => this.exportToAbstractQuery(query));
}
exportToAbstractQuery(query: Query): AbstractQuery {
const phlareQuery = query.labelSelector;
if (!phlareQuery || phlareQuery.length === 0) {
return { refId: query.refId, labelMatchers: [] };
}
const tokens = Prism.tokenize(phlareQuery, grammar);
return {
refId: query.refId,
labelMatchers: extractLabelMatchers(tokens),
};
}
}
const grammar: Grammar = {
'context-labels': {
pattern: /\{[^}]*(?=}?)/,
greedy: true,
inside: {
comment: {
pattern: /#.*/,
},
'label-key': {
pattern: /[a-zA-Z_]\w*(?=\s*(=|!=|=~|!~))/,
alias: 'attr-name',
greedy: true,
},
'label-value': {
pattern: /"(?:\\.|[^\\"])*"/,
greedy: true,
alias: 'attr-value',
},
punctuation: /[{]/,
},
},
punctuation: /[{}(),.]/,
};
@@ -181,7 +181,7 @@ export class TempoDatasource extends DataSourceWithBackend<TempoQuery, TempoJson
// Check whether this is a trace ID or traceQL query by checking if it only contains hex characters
if (queryValue.trim().match(hexOnlyRegex)) {
// There's only hex characters so let's assume that this is a trace ID
reportInteraction('grafana_traces_traceql_traceID_queried', {
reportInteraction('grafana_traces_traceID_queried', {
datasourceType: 'tempo',
app: options.app ?? '',
query: queryValue ?? '',
@@ -3,6 +3,7 @@ import type { languages } from 'monaco-editor';
import React, { useEffect, useRef } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime';
import { CodeEditor, Monaco, monacoTypes, useTheme2 } from '@grafana/ui';
import { createErrorNotification } from '../../../../core/copy/appNotification';
@@ -10,7 +11,7 @@ import { notifyApp } from '../../../../core/reducers/appNotification';
import { dispatch } from '../../../../store/store';
import { TempoDatasource } from '../datasource';
import { CompletionProvider } from './autocomplete';
import { CompletionProvider, CompletionType } from './autocomplete';
import { languageDefinition } from './traceql';
interface Props {
@@ -51,7 +52,7 @@ export function TraceQLEditor(props: Props) {
}}
onBeforeEditorMount={ensureTraceQL}
onEditorDidMount={(editor, monaco) => {
setupAutocompleteFn(editor, monaco);
setupAutocompleteFn(editor, monaco, setupRegisterInteractionCommand(editor));
setupActions(editor, monaco, onRunQuery);
setupPlaceholder(editor, monaco, styles);
setupAutoSize(editor);
@@ -101,6 +102,17 @@ function setupActions(editor: monacoTypes.editor.IStandaloneCodeEditor, monaco:
});
}
function setupRegisterInteractionCommand(editor: monacoTypes.editor.IStandaloneCodeEditor): string | null {
return editor.addCommand(0, function (_, label, type: CompletionType) {
const properties: Record<string, unknown> = { datasourceType: 'tempo', type };
// Filter out the label for TAG_VALUE completions to avoid potentially exposing sensitive data
if (type !== 'TAG_VALUE') {
properties.label = label;
}
reportInteraction('grafana_traces_traceql_completion', properties);
});
}
function setupAutoSize(editor: monacoTypes.editor.IStandaloneCodeEditor) {
const container = editor.getDomNode();
const updateHeight = () => {
@@ -156,9 +168,14 @@ function useAutocomplete(datasource: TempoDatasource) {
}, []);
// This should be run in monaco onEditorDidMount
return (editor: monacoTypes.editor.IStandaloneCodeEditor, monaco: Monaco) => {
return (
editor: monacoTypes.editor.IStandaloneCodeEditor,
monaco: Monaco,
registerInteractionCommandId: string | null
) => {
providerRef.current.editor = editor;
providerRef.current.monaco = monaco;
providerRef.current.setRegisterInteractionCommandId(registerInteractionCommandId);
const { dispose } = monaco.languages.registerCompletionItemProvider(langId, providerRef.current);
autocompleteDisposeFun.current = dispose;
@@ -13,9 +13,11 @@ interface Props {
*/
export class CompletionProvider implements monacoTypes.languages.CompletionItemProvider {
languageProvider: TempoLanguageProvider;
registerInteractionCommandId: string | null;
constructor(props: Props) {
this.languageProvider = props.languageProvider;
this.registerInteractionCommandId = null;
}
triggerCharacters = ['{', '.', '[', '(', '=', '~', ' ', '"'];
@@ -57,14 +59,19 @@ export class CompletionProvider implements monacoTypes.languages.CompletionItemP
// so that monaco keeps the order we use
const maxIndexDigits = items.length.toString().length;
const suggestions: monacoTypes.languages.CompletionItem[] = items.map((item, index) => {
const suggestion = {
const suggestion: monacoTypes.languages.CompletionItem = {
kind: getMonacoCompletionItemKind(item.type, this.monaco!),
label: item.label,
insertText: item.insertText,
sortText: index.toString().padStart(maxIndexDigits, '0'), // to force the order we have
range,
command: {
id: this.registerInteractionCommandId || 'noOp',
title: 'Report Interaction',
arguments: [item.label, item.type],
},
};
fixSuggestion(suggestion, item.type, model, offset, this.monaco!);
fixSuggestion(suggestion, item.type, model, offset);
return suggestion;
});
return { suggestions };
@@ -78,6 +85,13 @@ export class CompletionProvider implements monacoTypes.languages.CompletionItemP
tags.forEach((t) => (this.tags[t] = new Set<string>()));
}
/**
* Set the ID for the registerInteraction command, to be used to keep track of how many completions are used by the users
*/
setRegisterInteractionCommandId(id: string | null) {
this.registerInteractionCommandId = id;
}
private overrideTagName(tagName: string): string {
switch (tagName) {
case 'status':
@@ -88,7 +102,7 @@ export class CompletionProvider implements monacoTypes.languages.CompletionItemP
}
private async getTagValues(tagName: string): Promise<Array<SelectableValue<string>>> {
let tagValues: Array<SelectableValue<string>> = [];
let tagValues: Array<SelectableValue<string>>;
if (this.cachedValues.hasOwnProperty(tagName)) {
tagValues = this.cachedValues[tagName];
@@ -404,11 +418,10 @@ function getRangeAndOffset(monaco: Monaco, model: monacoTypes.editor.ITextModel,
* here.
*/
function fixSuggestion(
suggestion: monacoTypes.languages.CompletionItem & { range: monacoTypes.IRange },
suggestion: monacoTypes.languages.CompletionItem,
itemType: CompletionType,
model: monacoTypes.editor.ITextModel,
offset: number,
monaco: Monaco
offset: number
) {
if (itemType === 'TAG_NAME') {
const match = model
@@ -427,10 +440,10 @@ function fixSuggestion(
}
// Adjust the range, so that we will replace the whole tag.
suggestion.range = monaco.Range.lift({
suggestion.range = {
...suggestion.range,
startColumn: offset - tag.length + 1,
});
};
}
}
}
+44 -11
View File
@@ -1258,6 +1258,15 @@ __metadata:
languageName: node
linkType: hard
"@babel/helper-skip-transparent-expression-wrappers@npm:^7.20.0":
version: 7.20.0
resolution: "@babel/helper-skip-transparent-expression-wrappers@npm:7.20.0"
dependencies:
"@babel/types": ^7.20.0
checksum: 34da8c832d1c8a546e45d5c1d59755459ffe43629436707079989599b91e8c19e50e73af7a4bd09c95402d389266731b0d9c5f69e372d8ebd3a709c05c80d7dd
languageName: node
linkType: hard
"@babel/helper-split-export-declaration@npm:^7.15.4":
version: 7.15.4
resolution: "@babel/helper-split-export-declaration@npm:7.15.4"
@@ -1853,6 +1862,19 @@ __metadata:
languageName: node
linkType: hard
"@babel/plugin-proposal-optional-chaining@npm:7.20.7":
version: 7.20.7
resolution: "@babel/plugin-proposal-optional-chaining@npm:7.20.7"
dependencies:
"@babel/helper-plugin-utils": ^7.20.2
"@babel/helper-skip-transparent-expression-wrappers": ^7.20.0
"@babel/plugin-syntax-optional-chaining": ^7.8.3
peerDependencies:
"@babel/core": ^7.0.0-0
checksum: 274b8932335bd064ca24cf1a4da2b2c20c92726d4bfa8b0cb5023857479b8481feef33505c16650c7b9239334e5c6959babc924816324c4cf223dd91c7ca79bc
languageName: node
linkType: hard
"@babel/plugin-proposal-private-methods@npm:^7.12.1, @babel/plugin-proposal-private-methods@npm:^7.18.6":
version: 7.18.6
resolution: "@babel/plugin-proposal-private-methods@npm:7.18.6"
@@ -3669,6 +3691,17 @@ __metadata:
languageName: node
linkType: hard
"@babel/types@npm:^7.20.0":
version: 7.20.7
resolution: "@babel/types@npm:7.20.7"
dependencies:
"@babel/helper-string-parser": ^7.19.4
"@babel/helper-validator-identifier": ^7.19.1
to-fast-properties: ^2.0.0
checksum: b39af241f0b72bba67fd6d0d23914f6faec8c0eba8015c181cbd5ea92e59fc91a52a1ab490d3520c7dbd19ddb9ebb76c476308f6388764f16d8201e37fae6811
languageName: node
linkType: hard
"@babel/types@npm:^7.20.2, @babel/types@npm:^7.20.5":
version: 7.20.5
resolution: "@babel/types@npm:7.20.5"
@@ -12743,13 +12776,13 @@ __metadata:
languageName: node
linkType: hard
"@visx/event@npm:2.6.0":
version: 2.6.0
resolution: "@visx/event@npm:2.6.0"
"@visx/event@npm:2.17.0":
version: 2.17.0
resolution: "@visx/event@npm:2.17.0"
dependencies:
"@types/react": "*"
"@visx/point": 2.6.0
checksum: a3155bab7d964f592702981a2a6cae3742d9f23732ec64c2c3717c8b17350e22006fb13a96201cbe78bdd35170c8363ab645bf0c4c8f8dee41be64618fd38c47
"@visx/point": 2.17.0
checksum: 360a9ee2a163c0e0200f98cadcd9f0c98f53725d4be62e49d326aeb8ca7cd837305f28f326f03d9ae49910ca67e1185a97827f0ef76efb0b901fb54c56c97bd3
languageName: node
linkType: hard
@@ -12778,10 +12811,10 @@ __metadata:
languageName: node
linkType: hard
"@visx/point@npm:2.6.0":
version: 2.6.0
resolution: "@visx/point@npm:2.6.0"
checksum: 56be8e31abaf10a182d328141a0929ea28d2956d4c06469fc51cde3f30f64ae035aaa0771f44010c3484ae63c7cd1bc9b782cde6cfcf9713190c20b301ab181e
"@visx/point@npm:2.17.0":
version: 2.17.0
resolution: "@visx/point@npm:2.17.0"
checksum: 9cb18e41f1f47e46b85bdbbdd1c930a654ed8b731b3a5da0920e2b3a261d887c066ec97079293d4f5195a1e04d842faa2f2bf0519876cb27ea6ee82fc5ff8855
languageName: node
linkType: hard
@@ -21921,7 +21954,7 @@ __metadata:
"@babel/plugin-proposal-class-properties": 7.18.6
"@babel/plugin-proposal-nullish-coalescing-operator": 7.18.6
"@babel/plugin-proposal-object-rest-spread": 7.20.2
"@babel/plugin-proposal-optional-chaining": 7.18.9
"@babel/plugin-proposal-optional-chaining": 7.20.7
"@babel/plugin-syntax-dynamic-import": 7.8.3
"@babel/plugin-transform-react-constant-elements": 7.20.2
"@babel/plugin-transform-runtime": 7.19.6
@@ -22045,7 +22078,7 @@ __metadata:
"@types/webpack-env": 1.18.0
"@typescript-eslint/eslint-plugin": 5.42.0
"@typescript-eslint/parser": 5.42.0
"@visx/event": 2.6.0
"@visx/event": 2.17.0
"@visx/gradient": 2.10.0
"@visx/group": 2.10.0
"@visx/scale": 2.2.2