From ea27eca14712639a998235a095d07cf09ca9ff75 Mon Sep 17 00:00:00 2001 From: Gilles De Mey Date: Thu, 17 Nov 2022 21:41:46 +0100 Subject: [PATCH 001/153] Email: Use MJML email templates (#57751) Co-authored-by: Santiago --- devenv/README.md | 16 + emails/Makefile | 14 + emails/assets/css/ink.css | 688 ------- emails/assets/css/style.css | 196 -- emails/grunt/aliases.yaml | 11 +- emails/grunt/assemble.js | 11 +- emails/grunt/copy.js | 11 + emails/grunt/premailer.js | 31 - emails/grunt/processhtml.js | 12 - emails/grunt/replace.js | 2 +- emails/grunt/uncss.js | 9 - emails/grunt/watch.js | 16 - emails/package.json | 27 +- emails/templates/alert_notification.html | 134 -- emails/templates/alert_notification.txt | 26 - emails/templates/invited_to_org.html | 47 - emails/templates/invited_to_org.mjml | 40 + emails/templates/invited_to_org.txt | 2 +- emails/templates/layouts/default.html | 161 -- emails/templates/layouts/default.txt | 3 - emails/templates/new_user_invite.html | 49 - emails/templates/new_user_invite.mjml | 36 + emails/templates/ng_alert_notification.html | 271 --- emails/templates/ng_alert_notification.mjml | 122 ++ .../partials/alerting/firing_instance.mjml | 23 + .../partials/alerting/grouping_labels.mjml | 30 + .../partials/alerting/instance_details.mjml | 195 ++ .../partials/alerting/resolved_instance.mjml | 23 + .../templates/partials/alerting/summary.mjml | 28 + emails/templates/partials/layout/default.txt | 3 + emails/templates/partials/layout/footer.mjml | 5 + emails/templates/partials/layout/head.mjml | 10 + emails/templates/partials/layout/header.mjml | 3 + emails/templates/reset_password.html | 42 - emails/templates/reset_password.mjml | 36 + emails/templates/signup_started.html | 46 - emails/templates/signup_started.mjml | 39 + emails/templates/welcome_on_signup.html | 48 - emails/templates/welcome_on_signup.mjml | 40 + go.mod | 10 +- go.sum | 11 + .../ngalert/notifier/channels/email_test.go | 10 +- pkg/services/notifications/notifications.go | 2 + public/emails/alert_notification.txt | 3 +- public/emails/invited_to_org.html | 479 +++-- public/emails/invited_to_org.txt | 6 +- public/emails/new_user_invite.html | 475 ++--- public/emails/new_user_invite.txt | 6 +- public/emails/ng_alert_notification.html | 1663 +++++++++++++---- public/emails/ng_alert_notification.txt | 6 +- public/emails/reset_password.html | 468 ++--- public/emails/reset_password.txt | 6 +- public/emails/signup_started.html | 487 +++-- public/emails/signup_started.txt | 3 +- public/emails/welcome_on_signup.html | 479 +++-- public/emails/welcome_on_signup.txt | 6 +- 56 files changed, 3073 insertions(+), 3553 deletions(-) create mode 100644 emails/Makefile delete mode 100644 emails/assets/css/ink.css delete mode 100644 emails/assets/css/style.css create mode 100644 emails/grunt/copy.js delete mode 100644 emails/grunt/premailer.js delete mode 100644 emails/grunt/processhtml.js delete mode 100644 emails/grunt/uncss.js delete mode 100644 emails/grunt/watch.js delete mode 100644 emails/templates/alert_notification.html delete mode 100644 emails/templates/alert_notification.txt delete mode 100644 emails/templates/invited_to_org.html create mode 100644 emails/templates/invited_to_org.mjml delete mode 100644 emails/templates/layouts/default.html delete mode 100644 emails/templates/layouts/default.txt delete mode 100644 emails/templates/new_user_invite.html create mode 100644 emails/templates/new_user_invite.mjml delete mode 100644 emails/templates/ng_alert_notification.html create mode 100644 emails/templates/ng_alert_notification.mjml create mode 100644 emails/templates/partials/alerting/firing_instance.mjml create mode 100644 emails/templates/partials/alerting/grouping_labels.mjml create mode 100644 emails/templates/partials/alerting/instance_details.mjml create mode 100644 emails/templates/partials/alerting/resolved_instance.mjml create mode 100644 emails/templates/partials/alerting/summary.mjml create mode 100644 emails/templates/partials/layout/default.txt create mode 100644 emails/templates/partials/layout/footer.mjml create mode 100644 emails/templates/partials/layout/head.mjml create mode 100644 emails/templates/partials/layout/header.mjml delete mode 100644 emails/templates/reset_password.html create mode 100644 emails/templates/reset_password.mjml delete mode 100644 emails/templates/signup_started.html create mode 100644 emails/templates/signup_started.mjml delete mode 100644 emails/templates/welcome_on_signup.html create mode 100644 emails/templates/welcome_on_signup.mjml diff --git a/devenv/README.md b/devenv/README.md index 6d6a7065b87..b8cbc39c8e9 100644 --- a/devenv/README.md +++ b/devenv/README.md @@ -4,6 +4,7 @@ This folder contains useful scripts and configuration so you can: - Configure data sources in Grafana for development. - Configure dashboards for development and test scenarios. +- Set up an SMTP Server + Web Interface for viewing and testing emails. - Create docker-compose file with databases and fake data. ## Install Docker @@ -58,6 +59,21 @@ Jaeger block runs both Jaeger and Loki container. Loki container sends traces to | 1.0 | graphite1 | 8280 | 2203 | 2203 | | 0.9 | graphite09 | 8380 | 2303 | 2303 | +#### MailDev + +MailDev block runs an SMTP server and a web UI to test and view emails. This is useful for testing your email notifications locally. + +Make sure you configure your .ini file with the following settings: + +```ini +[smtp] +enabled = true +skip_verify = true +host = "localhost:1025" +``` + +You can access the web UI at http://localhost:12080/#/ + ## Debugging setup in VS Code An example of launch.json is provided in `devenv/vscode/launch.json`. It basically does what Makefile and .bra.toml do. The 'program' field is set to the folder name so VS Code loads all *.go files in it instead of just main.go. diff --git a/emails/Makefile b/emails/Makefile new file mode 100644 index 00000000000..e44bd04b9a1 --- /dev/null +++ b/emails/Makefile @@ -0,0 +1,14 @@ +build: build-html build-txt + +build-html: + npx mjml \ + --config.beautify true \ + --config.minify false \ + --config.validationLevel=strict \ + --config.keepComments=false \ + ./templates/*.mjml --output ../public/emails/ + +build-txt: + npx grunt + +.PHONY: build build-html build-txt diff --git a/emails/assets/css/ink.css b/emails/assets/css/ink.css deleted file mode 100644 index f4c1a291cb3..00000000000 --- a/emails/assets/css/ink.css +++ /dev/null @@ -1,688 +0,0 @@ -/********************************************** -* Ink v1.0.5 - Copyright 2013 ZURB Inc * -**********************************************/ - -/* Client-specific Styles & Reset */ - -#outlook a { - padding:0; -} - -body{ - width:100% !important; - min-width: 100%; - -webkit-text-size-adjust:100%; - -ms-text-size-adjust:100%; - margin:0; - padding:0; -} - - - -.ExternalClass { - width:100%; -} - -.ExternalClass, -.ExternalClass p, -.ExternalClass span, -.ExternalClass font, -.ExternalClass td, -.ExternalClass div { - line-height: 100%; -} - -#backgroundTable { - margin:0; - padding:0; - width:100% !important; - line-height: 100% !important; -} - -img { - outline:none; - text-decoration:none; - -ms-interpolation-mode: bicubic; - width: auto; - float: left; - clear: both; - display: block; -} - -center { - width: 100%; - min-width: 580px; -} - -a img { - border: none; -} - -p { - margin: 0 0 0 10px; -} - -table { - border-spacing: 0; - border-collapse: collapse; -} - -td { - word-break: break-word; - -webkit-hyphens: auto; - -moz-hyphens: auto; - hyphens: auto; - border-collapse: collapse !important; -} - -table, tr, td { - padding: 0; - vertical-align: top; - text-align: left; -} - -hr { - color: #d9d9d9; - background-color: #d9d9d9; - height: 1px; - border: none; -} - -/* Responsive Grid */ - -table.body { - height: 100%; - width: 100%; -} - -table.container { - width: 580px; - margin: 0 auto; - text-align: inherit; -} - -table.row { - padding: 0px; - width: 100%; - position: relative; -} - -table.container table.row { - display: block; -} - -td.wrapper { - padding: 10px 20px 0px 0px; - position: relative; -} - -table.columns, -table.column { - margin: 0 auto; -} - -table.columns td, -table.column td { - padding: 0px 0px 10px; -} - -table.columns td.sub-columns, -table.column td.sub-columns, -table.columns td.sub-column, -table.column td.sub-column { - padding-right: 10px; -} - -td.sub-column, td.sub-columns { - min-width: 0px; -} - -table.row td.last, -table.container td.last { - padding-right: 0px; -} - -table.one { width: 30px; } -table.two { width: 80px; } -table.three { width: 130px; } -table.four { width: 180px; } -table.five { width: 230px; } -table.six { width: 280px; } -table.seven { width: 330px; } -table.eight { width: 380px; } -table.nine { width: 430px; } -table.ten { width: 480px; } -table.eleven { width: 530px; } -table.twelve { width: 580px; } - -table.one center { min-width: 30px; } -table.two center { min-width: 80px; } -table.three center { min-width: 130px; } -table.four center { min-width: 180px; } -table.five center { min-width: 230px; } -table.six center { min-width: 280px; } -table.seven center { min-width: 330px; } -table.eight center { min-width: 380px; } -table.nine center { min-width: 430px; } -table.ten center { min-width: 480px; } -table.eleven center { min-width: 530px; } -table.twelve center { min-width: 580px; } - -table.one .panel center { min-width: 10px; } -table.two .panel center { min-width: 60px; } -table.three .panel center { min-width: 110px; } -table.four .panel center { min-width: 160px; } -table.five .panel center { min-width: 210px; } -table.six .panel center { min-width: 260px; } -table.seven .panel center { min-width: 310px; } -table.eight .panel center { min-width: 360px; } -table.nine .panel center { min-width: 410px; } -table.ten .panel center { min-width: 460px; } -table.eleven .panel center { min-width: 510px; } -table.twelve .panel center { min-width: 560px; } - -.body .columns td.one, -.body .column td.one { width: 8.333333%; } -.body .columns td.two, -.body .column td.two { width: 16.666666%; } -.body .columns td.three, -.body .column td.three { width: 25%; } -.body .columns td.four, -.body .column td.four { width: 33.333333%; } -.body .columns td.five, -.body .column td.five { width: 41.666666%; } -.body .columns td.six, -.body .column td.six { width: 50%; } -.body .columns td.seven, -.body .column td.seven { width: 58.333333%; } -.body .columns td.eight, -.body .column td.eight { width: 66.666666%; } -.body .columns td.nine, -.body .column td.nine { width: 75%; } -.body .columns td.ten, -.body .column td.ten { width: 83.333333%; } -.body .columns td.eleven, -.body .column td.eleven { width: 91.666666%; } -.body .columns td.twelve, -.body .column td.twelve { width: 100%; } - -td.offset-by-one { padding-left: 50px; } -td.offset-by-two { padding-left: 100px; } -td.offset-by-three { padding-left: 150px; } -td.offset-by-four { padding-left: 200px; } -td.offset-by-five { padding-left: 250px; } -td.offset-by-six { padding-left: 300px; } -td.offset-by-seven { padding-left: 350px; } -td.offset-by-eight { padding-left: 400px; } -td.offset-by-nine { padding-left: 450px; } -td.offset-by-ten { padding-left: 500px; } -td.offset-by-eleven { padding-left: 550px; } - -td.expander { - visibility: hidden; - width: 0px; - padding: 0 !important; -} - -table.columns .text-pad, -table.column .text-pad { - padding-left: 10px; - padding-right: 10px; -} - -table.columns .left-text-pad, -table.columns .text-pad-left, -table.column .left-text-pad, -table.column .text-pad-left { - padding-left: 10px; -} - -table.columns .right-text-pad, -table.columns .text-pad-right, -table.column .right-text-pad, -table.column .text-pad-right { - padding-right: 10px; -} - -/* Block Grid */ - -.block-grid { - width: 100%; - max-width: 580px; -} - -.block-grid td { - display: inline-block; - padding:10px; -} - -.two-up td { - width:270px; -} - -.three-up td { - width:173px; -} - -.four-up td { - width:125px; -} - -.five-up td { - width:96px; -} - -.six-up td { - width:76px; -} - -.seven-up td { - width:62px; -} - -.eight-up td { - width:52px; -} - -/* Alignment & Visibility Classes */ - -table.center, td.center { - text-align: center; -} - -h1.center, -h2.center, -h3.center, -h4.center, -h5.center, -h6.center { - text-align: center; -} - -span.center { - display: block; - width: 100%; - text-align: center; -} - -img.center { - margin: 0 auto; - float: none; -} - -.show-for-small, -.hide-for-desktop { - display: none; -} - -/* Typography */ - -body, table.body, h1, h2, h3, h4, h5, h6, p, td { - color: #222222; - font-family: "Helvetica", "Arial", sans-serif; - font-weight: normal; - padding:0; - margin: 0; - text-align: left; - line-height: 1.3; -} - -h1, h2, h3, h4, h5, h6 { - word-break: normal; -} - -h1 {font-size: 40px;} -h2 {font-size: 36px;} -h3 {font-size: 32px;} -h4 {font-size: 28px;} -h5 {font-size: 24px;} -h6 {font-size: 20px;} -body, table.body, p, td {font-size: 14px;line-height:19px;} - -p.lead, p.lede, p.leed { - font-size: 18px; - line-height:21px; -} - -p { - margin-bottom: 10px; -} - -small { - font-size: 10px; -} - -a { - color: #2ba6cb; - text-decoration: none; -} - -a:hover { - color: #2795b6 !important; -} - -a:active { - color: #2795b6 !important; -} - -a:visited { - color: #2ba6cb !important; -} - -h1 a, -h2 a, -h3 a, -h4 a, -h5 a, -h6 a { - color: #2ba6cb; -} - -h1 a:active, -h2 a:active, -h3 a:active, -h4 a:active, -h5 a:active, -h6 a:active { - color: #2ba6cb !important; -} - -h1 a:visited, -h2 a:visited, -h3 a:visited, -h4 a:visited, -h5 a:visited, -h6 a:visited { - color: #2ba6cb !important; -} - -/* Panels */ - -.panel { - background: #f2f2f2; - border: 1px solid #d9d9d9; - padding: 10px !important; -} - -.sub-grid table { - width: 100%; -} - -.sub-grid td.sub-columns { - padding-bottom: 0; -} - -/* Buttons */ - -table.button, -table.tiny-button, -table.small-button, -table.medium-button, -table.large-button { - width: 100%; - overflow: hidden; -} - -table.button td, -table.tiny-button td, -table.small-button td, -table.medium-button td, -table.large-button td { - display: block; - width: auto !important; - text-align: center; - background: #2ba6cb; - border: 1px solid #2284a1; - color: #ffffff; - padding: 8px 0; -} - -table.tiny-button td { - padding: 5px 0 4px; -} - -table.small-button td { - padding: 8px 0 7px; -} - -table.medium-button td { - padding: 12px 0 10px; -} - -table.large-button td { - padding: 21px 0 18px; -} - -table.button td a, -table.tiny-button td a, -table.small-button td a, -table.medium-button td a, -table.large-button td a { - font-weight: bold; - text-decoration: none; - font-family: Helvetica, Arial, sans-serif; - color: #ffffff; - font-size: 16px; -} - -table.tiny-button td a { - font-size: 12px; - font-weight: normal; -} - -table.small-button td a { - font-size: 16px; -} - -table.medium-button td a { - font-size: 20px; -} - -table.large-button td a { - font-size: 24px; -} - -table.button:hover td, -table.button:visited td, -table.button:active td { - background: #2795b6 !important; -} - -table.button:hover td a, -table.button:visited td a, -table.button:active td a { - color: #fff !important; -} - -table.button:hover td, -table.tiny-button:hover td, -table.small-button:hover td, -table.medium-button:hover td, -table.large-button:hover td { - background: #2795b6 !important; -} - -table.button:hover td a, -table.button:active td a, -table.button td a:visited, -table.tiny-button:hover td a, -table.tiny-button:active td a, -table.tiny-button td a:visited, -table.small-button:hover td a, -table.small-button:active td a, -table.small-button td a:visited, -table.medium-button:hover td a, -table.medium-button:active td a, -table.medium-button td a:visited, -table.large-button:hover td a, -table.large-button:active td a, -table.large-button td a:visited { - color: #ffffff !important; -} - -table.secondary td { - background: #e9e9e9; - border-color: #d0d0d0; - color: #555; -} - -table.secondary td a { - color: #555; -} - -table.secondary:hover td { - background: #d0d0d0 !important; - color: #555; -} - -table.secondary:hover td a, -table.secondary td a:visited, -table.secondary:active td a { - color: #555 !important; -} - -table.success td { - background: #5da423; - border-color: #457a1a; -} - -table.success:hover td { - background: #457a1a !important; -} - -table.alert td { - background: #c60f13; - border-color: #970b0e; -} - -table.alert:hover td { - background: #970b0e !important; -} - -table.radius td { - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - border-radius: 3px; -} - -table.round td { - -webkit-border-radius: 500px; - -moz-border-radius: 500px; - border-radius: 500px; -} - -/* Outlook First */ - -body.outlook p { - display: inline !important; -} - -/* Media Queries */ - -@media only screen and (max-width: 600px) { - - table[class="body"] img { - - } - - table[class="body"] center { - min-width: 0 !important; - } - - table[class="body"] .container { - width: 95% !important; - } - - table[class="body"] .row { - width: 100% !important; - display: block !important; - } - - table[class="body"] .wrapper { - display: block !important; - padding-right: 0 !important; - } - - table[class="body"] .columns, - table[class="body"] .column { - table-layout: fixed !important; - float: none !important; - width: 100% !important; - padding-right: 0px !important; - padding-left: 0px !important; - display: block !important; - } - - table[class="body"] .wrapper.first .columns, - table[class="body"] .wrapper.first .column { - display: table !important; - } - - table[class="body"] table.columns td, - table[class="body"] table.column td { - width: 100% !important; - } - - table[class="body"] .columns td.one, - table[class="body"] .column td.one { width: 8.333333% !important; } - table[class="body"] .columns td.two, - table[class="body"] .column td.two { width: 16.666666% !important; } - table[class="body"] .columns td.three, - table[class="body"] .column td.three { width: 25% !important; } - table[class="body"] .columns td.four, - table[class="body"] .column td.four { width: 33.333333% !important; } - table[class="body"] .columns td.five, - table[class="body"] .column td.five { width: 41.666666% !important; } - table[class="body"] .columns td.six, - table[class="body"] .column td.six { width: 50% !important; } - table[class="body"] .columns td.seven, - table[class="body"] .column td.seven { width: 58.333333% !important; } - table[class="body"] .columns td.eight, - table[class="body"] .column td.eight { width: 66.666666% !important; } - table[class="body"] .columns td.nine, - table[class="body"] .column td.nine { width: 75% !important; } - table[class="body"] .columns td.ten, - table[class="body"] .column td.ten { width: 83.333333% !important; } - table[class="body"] .columns td.eleven, - table[class="body"] .column td.eleven { width: 91.666666% !important; } - table[class="body"] .columns td.twelve, - table[class="body"] .column td.twelve { width: 100% !important; } - - table[class="body"] td.offset-by-one, - table[class="body"] td.offset-by-two, - table[class="body"] td.offset-by-three, - table[class="body"] td.offset-by-four, - table[class="body"] td.offset-by-five, - table[class="body"] td.offset-by-six, - table[class="body"] td.offset-by-seven, - table[class="body"] td.offset-by-eight, - table[class="body"] td.offset-by-nine, - table[class="body"] td.offset-by-ten, - table[class="body"] td.offset-by-eleven { - padding-left: 0 !important; - } - - table[class="body"] table.columns td.expander { - width: 1px !important; - } - - table[class="body"] .right-text-pad, - table[class="body"] .text-pad-right { - padding-left: 10px !important; - } - - table[class="body"] .left-text-pad, - table[class="body"] .text-pad-left { - padding-right: 10px !important; - } - - table[class="body"] .hide-for-small, - table[class="body"] .show-for-desktop { - display: none !important; - } - - table[class="body"] .show-for-small, - table[class="body"] .hide-for-desktop { - display: inherit !important; - } -} diff --git a/emails/assets/css/style.css b/emails/assets/css/style.css deleted file mode 100644 index 065421a173d..00000000000 --- a/emails/assets/css/style.css +++ /dev/null @@ -1,196 +0,0 @@ - -body, table.body, h1, h2, h3, h4, h5, h6, p, td { - font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; - -webkit-font-smoothing: antialiased; - -webkit-text-size-adjust: none; -} - -h1 {font-size: 40px;} -h2 {font-size: 36px;} -h3 { - font-size: 22px; - margin-top: 10px; - margin-bottom: 10px; -} -h4 {font-size: 20px;} -h5 {font-size: 18px;} -h6 {font-size: 16px;} - -.emphasis { - font-weight: 600; -} - -a { - color: #E67612; - text-decoration: none; -} - -a:hover { - color: #ff8f2b !important; -} - -a:active { - color: #F2821E !important; -} - -a:visited { - color: #E67612 !important; -} - -table.facebook td { - background: #3b5998; - border-color: #2d4473; -} - -table.facebook:hover td { - background: #2d4473 !important; -} - -table.twitter td { - background: #00acee; - border-color: #0087bb; -} - -table.twitter:hover td { - background: #0087bb !important; -} - -table.google-plus td { - background-color: #DB4A39; - border-color: #CC0000; -} - -table.google-plus:hover td { - background: #CC0000 !important; -} - -.template-label { - color: #ffffff; - font-weight: bold; - font-size: 11px; -} - -.callout .wrapper { - padding-bottom: 20px; -} - -.callout .panel { - background: #ECF8FF; - border-color: #b9e5ff; -} - -.header { -margin-top:25px; -margin-bottom: 25px; -} - -.data { - font-size: 16px; -} - -.footer { - background-color: #2e2e2e; - color: #999999; - margin: 0 auto; - width: 100%; -} - -@media only screen and (max-width: 600px) { - table[class="body"] .right-text-pad { - padding-left: 10px !important; - } - - table[class="body"] .left-text-pad { - padding-right: 10px !important; - } - - .logo { - margin-left: 10px; - } -} - -table.better-button { - margin-top: 10px; - margin-bottom: 20px; -} - -table.columns td.better-button { - -webkit-border-radius: 2px; - -moz-border-radius: 2px; - border-radius: 2px; - padding-bottom: 0px; -} - -.better-button a { - text-decoration: none; - -webkit-border-radius: 2px; - -moz-border-radius: 2px; - border-radius: 2px; - - padding: 12px 25px; - border: 1px solid #ff8f2b; - display: inline-block; - color: #FFF; -} - -.better-button:hover a { - color: #FFFFFF !important; - background-color: #F2821E; - border: 1px solid #F2821E; -} - -.better-button:visited a { - color: #FFFFFF !important; -} - -.better-button:active a { - color: #FFFFFF !important; -} - -table.better-button-alt { - margin-top: 10px; - margin-bottom: 20px; -} - -table.columns td.better-button-alt { - -webkit-border-radius: 2px; - -moz-border-radius: 2px; - border-radius: 2px; - padding-bottom: 0px; -} - -.better-button-alt a { - text-decoration: none; - -webkit-border-radius: 2px; - -moz-border-radius: 2px; - border-radius: 2px; - - padding: 12px 25px; - border: 1px solid #ff8f2b; - background-color: #EFEFEF; - display: inline-block; - color: #ff8f2b; -} - -.better-button-alt:hover a { - color: #ff8f2b !important; - background-color: #DDDDDD; - border: 1px solid #F2821E; -} - -.better-button-alt:visited a { - color: #ff8f2b !important; -} - -.better-button-alt:active a { - color: #ff8f2b !important; -} - -.verification-code { - background-color: #EEEEEE; - padding: 3px; - margin: 8px; - display: inline-block; - font-weight: bold; - font-size: 20px; -} diff --git a/emails/grunt/aliases.yaml b/emails/grunt/aliases.yaml index 6a2e47777cd..550d7a5422a 100644 --- a/emails/grunt/aliases.yaml +++ b/emails/grunt/aliases.yaml @@ -1,8 +1,5 @@ - default: - - 'clean' - - 'assemble' - - 'replace' - - 'uncss' - - 'processhtml' - - 'premailer' + - 'clean' + - 'assemble' + - 'replace' + - 'copy' diff --git a/emails/grunt/assemble.js b/emails/grunt/assemble.js index 6ef46860267..e0d1f103a42 100644 --- a/emails/grunt/assemble.js +++ b/emails/grunt/assemble.js @@ -2,21 +2,12 @@ module.exports = function () { 'use strict'; return { options: { - partials: ['templates/partials/*.hbs'], - helpers: ['templates/helpers/**/*.js'], data: [], flatten: true, }, - html: { - options: { - layout: 'templates/layouts/default.html', - }, - src: ['templates/*.html'], - dest: 'dist/', - }, txt: { options: { - layout: 'templates/layouts/default.txt', + layout: 'templates/partials/layout/default.txt', ext: '.txt', }, src: ['templates/*.txt'], diff --git a/emails/grunt/copy.js b/emails/grunt/copy.js new file mode 100644 index 00000000000..4f794d85a3a --- /dev/null +++ b/emails/grunt/copy.js @@ -0,0 +1,11 @@ +module.exports = function () { + 'use strict'; + return { + txt: { + expand: true, + cwd: 'dist', + src: ['**.txt'], + dest: '../public/emails/', + }, + }; +}; diff --git a/emails/grunt/premailer.js b/emails/grunt/premailer.js deleted file mode 100644 index f587095c256..00000000000 --- a/emails/grunt/premailer.js +++ /dev/null @@ -1,31 +0,0 @@ -module.exports = { - html: { - options: { - verbose: true, - removeComments: true, - }, - files: [ - { - expand: true, // Enable dynamic expansion. - cwd: 'dist', // Src matches are relative to this path. - src: ['*.html'], // Actual pattern(s) to match. - dest: '../public/emails/', // Destination path prefix. - }, - ], - }, - txt: { - options: { - verbose: true, - mode: 'txt', - lineLength: 90, - }, - files: [ - { - expand: true, // Enable dynamic expansion. - cwd: 'dist', // Src matches are relative to this path. - src: ['*.txt'], // Actual patterns to match. - dest: '../public/emails/', // Destination path prefix. - }, - ], - }, -}; diff --git a/emails/grunt/processhtml.js b/emails/grunt/processhtml.js deleted file mode 100644 index 777b2d27d73..00000000000 --- a/emails/grunt/processhtml.js +++ /dev/null @@ -1,12 +0,0 @@ -module.exports = { - dist: { - files: [ - { - expand: true, // Enable dynamic expansion. - cwd: 'dist', // Src matches are relative to this path. - src: ['*.html'], // Actual pattern(s) to match. - dest: 'dist/', // Destination path prefix. - }, - ], - }, -}; diff --git a/emails/grunt/replace.js b/emails/grunt/replace.js index 0d8c030d2f3..be4f18e34d4 100644 --- a/emails/grunt/replace.js +++ b/emails/grunt/replace.js @@ -1,7 +1,7 @@ module.exports = { dist: { overwrite: true, - src: ['dist/*.html', 'dist/*.txt'], + src: ['dist/*.txt'], replacements: [ { from: '[[', diff --git a/emails/grunt/uncss.js b/emails/grunt/uncss.js deleted file mode 100644 index c1ec535e1bb..00000000000 --- a/emails/grunt/uncss.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = { - dist: { - src: ['dist/*.html'], - dest: 'dist/css/tidy.css', - options: { - report: 'min', // optional: include to report savings - }, - }, -}; diff --git a/emails/grunt/watch.js b/emails/grunt/watch.js deleted file mode 100644 index b071320b3e6..00000000000 --- a/emails/grunt/watch.js +++ /dev/null @@ -1,16 +0,0 @@ -module.exports = { - src: { - files: [ - //what are the files that we want to watch - 'assets/css/*.css', - 'templates/**/*.html', - 'templates/**/*.txt', - 'grunt/*.js', - ], - tasks: ['default'], - options: { - nospawn: true, - livereload: false, - }, - }, -}; diff --git a/emails/package.json b/emails/package.json index f4eb62d4eae..c8de033b507 100644 --- a/emails/package.json +++ b/emails/package.json @@ -1,26 +1,17 @@ { - "name": "Grafana-Email-Campaign", + "name": "grafana-email-campaign", "version": "1.0.0", - "description": "Grafana Email templates based on Zurb Ink", - "repository": "dnnsldr/", - "author": { - "name": "dnnsldr", - "email": "delder@riester.com", - "url": "https://github.com/dnnsldr" - }, - "scripts": { - "build": "grunt", - "start": "grunt watch" - }, + "description": "Grafana Email templates based on MJML", + "author": "Grafana Labs", "devDependencies": { "grunt": "1.0.1", - "grunt-premailer": "1.1.0", - "grunt-processhtml": "^0.4.2", - "grunt-uncss": "0.9.0", - "load-grunt-config": "3.0.1", + "grunt-assemble": "0.6.3", + "grunt-cli": "^1.4.3", + "grunt-contrib-clean": "2.0.0", + "grunt-contrib-copy": "^1.0.0", "grunt-contrib-watch": "1.1.0", "grunt-text-replace": "0.4.0", - "grunt-assemble": "0.6.3", - "grunt-contrib-clean": "2.0.0" + "load-grunt-config": "3.0.1", + "mjml": "^4.13.0" } } diff --git a/emails/templates/alert_notification.html b/emails/templates/alert_notification.html deleted file mode 100644 index 98d2f7ff56c..00000000000 --- a/emails/templates/alert_notification.html +++ /dev/null @@ -1,134 +0,0 @@ -[[Subject .Subject "[[.Title]]"]] - - - - - -
- - - - -
-

[[.Title]]

-
-
- - - - - -
- - - - -
-

[[.Message]]

-
-
- -[[if ne .Error "" ]] - - - - -
-
- - - - - - - -
-
Error message
-
-

[[.Error]]

-
-
-
-[[end]] - -[[if ne .State "ok" ]] - - - - -
-
- - - - - - [[range .EvalMatches]] - - - - - [[end]] -
-
Metric name
-
-
Value
-
-
[[.Metric]]
-
-
[[.Value]]
-
-
-
-[[end]] - - - - - -
- - - - -
- [[if ne .ImageLink "" ]] - Alerting Panel - [[end]] - [[if ne .EmbeddedImage "" ]] - Alerting Panel - [[end]] -
-
- - - - - - -
- - - - - -
- - - - -
- View your Alert rule -
-
- - - - -
- Go to the Alerts page -
-
-
- - diff --git a/emails/templates/alert_notification.txt b/emails/templates/alert_notification.txt deleted file mode 100644 index 92b8d91386e..00000000000 --- a/emails/templates/alert_notification.txt +++ /dev/null @@ -1,26 +0,0 @@ -[[Subject .Subject "[[.Title]]"]] - -[[.Title]] ----------------- - -[[.Message]] - -[[if ne .Error "" ]] -Error message: -[[.Error]] -[[end]] - -[[if ne .State "ok" ]] -[[range .EvalMatches]] -Metric name: -[[.Metric]] -Value: -[[.Value]] -[[end]] -[[end]] - -View your Alert rule: -[[.RuleUrl]]" - -Go to the Alerts page: -[[.AlertPageUrl]] diff --git a/emails/templates/invited_to_org.html b/emails/templates/invited_to_org.html deleted file mode 100644 index 69ef61587df..00000000000 --- a/emails/templates/invited_to_org.html +++ /dev/null @@ -1,47 +0,0 @@ - - -[[Subject .Subject "[[.InvitedBy]] has added you to the [[.OrgName]] organization"]] - - - - - -
- - - - - - -
-

You have been added to [[.OrgName]]

-
- -
- - - - - -
- - - - - - - - -
-

[[.InvitedBy]] has added you to the [[.OrgName]] organization in Grafana. -

Once logged in, [[.OrgName]] will be available in the left side menu, in the dropdown below your username.

-
- - - - -
Log in now
-
-
- - diff --git a/emails/templates/invited_to_org.mjml b/emails/templates/invited_to_org.mjml new file mode 100644 index 00000000000..d85a011dbc5 --- /dev/null +++ b/emails/templates/invited_to_org.mjml @@ -0,0 +1,40 @@ + + + + + {{ Subject .Subject "{{ .InvitedBy }} has added you to the {{ .OrgName }} organization" }} + + + + + + + + + + +

You have been added to {{ .OrgName }}

+ {{ .InvitedBy }} has added you to the {{ .OrgName }} organization in Grafana. +
+ + Once logged in, {{ .OrgName }} will be available to switch to in the user interface. + + + Log in now by clicking the link below: + + + Login to Grafana + + + You can also copy and paste this link into your browser directly: + + + {{ .AppUrl }} + +
+
+ + + +
+
diff --git a/emails/templates/invited_to_org.txt b/emails/templates/invited_to_org.txt index 322119aa942..b2ae9e8a1ac 100644 --- a/emails/templates/invited_to_org.txt +++ b/emails/templates/invited_to_org.txt @@ -6,4 +6,4 @@ You have been added to [[.OrgName]] Once logged in, [[.OrgName]] will be available in the left side menu, in the dropdown below your username. Log in now: -[[.AppUrl]] \ No newline at end of file +[[.AppUrl]] diff --git a/emails/templates/layouts/default.html b/emails/templates/layouts/default.html deleted file mode 100644 index 7e580ac910f..00000000000 --- a/emails/templates/layouts/default.html +++ /dev/null @@ -1,161 +0,0 @@ - - - - - - - - - - - - - - - - -
-
- - - - -
-
- - - - - -
- - - - - - -
- -
- -
- -
-
- - - - - - - - -
- {{> body }} - -
- - - - - - -
-
- - diff --git a/emails/templates/layouts/default.txt b/emails/templates/layouts/default.txt deleted file mode 100644 index 543f4ac140d..00000000000 --- a/emails/templates/layouts/default.txt +++ /dev/null @@ -1,3 +0,0 @@ -{{> body }} - -Sent by Grafana v[[.BuildVersion]] (c) 2022 Grafana Labs \ No newline at end of file diff --git a/emails/templates/new_user_invite.html b/emails/templates/new_user_invite.html deleted file mode 100644 index b2c0b431fd3..00000000000 --- a/emails/templates/new_user_invite.html +++ /dev/null @@ -1,49 +0,0 @@ - - -[[Subject .Subject "[[.InvitedBy]] has invited you to join Grafana"]] - - - - - -
- - - - - - -
-

You're invited to join [[.OrgName]]

-
- -
- - - - - -
- - - - - - - - - - - -
-

You've been invited to join the [[.OrgName]] organization by [[.InvitedBy]]. To accept your invitation and join the team, please click the link below:

-
- - - - -
Accept Invitation
-
-

You can also copy and paste this link into your browser directly: [[.LinkUrl]]

-
-
diff --git a/emails/templates/new_user_invite.mjml b/emails/templates/new_user_invite.mjml new file mode 100644 index 00000000000..66c28de9454 --- /dev/null +++ b/emails/templates/new_user_invite.mjml @@ -0,0 +1,36 @@ + + + + + {{ Subject .Subject "{{ .InvitedBy }} has invited you to join Grafana" }} + + + + + + + + + + +

You're invited to join {{ .OrgName }}

+
+ + You've been invited to join the {{ .OrgName }} organization by {{ .InvitedBy }}. To accept your invitation and join the team, please click the link below: + + + Accept Invitation + + + You can also copy and paste this link into your browser directly: + + + {{ .LinkUrl }} + +
+
+ + + +
+
diff --git a/emails/templates/ng_alert_notification.html b/emails/templates/ng_alert_notification.html deleted file mode 100644 index 093c75cc21e..00000000000 --- a/emails/templates/ng_alert_notification.html +++ /dev/null @@ -1,271 +0,0 @@ - -
- -[[Subject .Subject "[[.Title]]"]] - -[[ define "__text_values_list" ]][[ $len := len .Values ]][[ if $len ]][[ $first := gt $len 1 ]][[ range $refID, $value := .Values -]] -[[ $refID ]]=[[ $value ]][[ if $first ]], [[ end ]][[ $first = false ]][[ end -]] -[[ else ]][no value][[ end ]][[ end ]] - -[[ define "alert" ]] - - [[ if ne .ImageURL "" ]] - - - Alerting Panel - - - [[ end ]] - [[ if ne .EmbeddedImage "" ]] - - - Alerting Chart Attached Below - - - [[ end ]] - - - Value: [[ template "__text_values_list" . ]] - - - [[ if gt (len .Annotations.SortedPairs) 0 ]] - - - [[ range .Annotations.SortedPairs ]] -

[[ .Name ]]: [[ .Value ]]

- [[ end ]] - - - [[ end ]] - - - Labels: -
    - [[ range .Labels.SortedPairs ]]
  • [[ .Name ]]: [[ .Value ]]
  • [[ end ]] -
- - - - - [[ if .SilenceURL ]] - - - Silence - - [[ end ]] - [[ if .Annotations.runbook_url ]] - - - View Runbook - - [[ end ]] - [[ if .DashboardURL]] - - - Go to Dashboard - - [[ end ]] - [[ if .PanelURL]] - - - Go to Panel - - [[ end ]] - [[ if gt (len .GeneratorURL) 0 ]]Source[[ end ]] - - - - -
-
-
- - -[[ end ]] - -[[ if gt (len .Message) 0 ]] -
[[ .Message ]] -[[ else ]] - - - - - - - -
- - [[ if gt (len .Alerts.Firing) 0 ]] - - - - [[ range .Alerts.Firing ]] - - - - - [[ template "alert" . ]] - [[ end ]] - [[ end ]] - [[ if gt (len .Alerts.Resolved) 0 ]] - - - - [[ range .Alerts.Resolved ]] - - - - - [[ template "alert" . ]] - [[ end ]] - [[ end ]] - - - -
- Firing: [[ .Alerts.Firing | len ]] alert[[ if gt (len .Alerts.Firing) 1 ]]s[[ end ]][[ if gt (len .GroupLabels.SortedPairs) 1 ]] for - [[ range .GroupLabels.SortedPairs ]] - [[ .Name ]]=[[ .Value ]] - [[ end ]][[ end ]] -
- Firing - - [[ .Labels.alertname ]] -
- Resolved: [[ .Alerts.Resolved | len ]] alert[[ if gt (len .Alerts.Resolved) 1 ]]s[[ end ]][[ if gt (len .GroupLabels.SortedPairs) 1 ]] for - [[ range .GroupLabels.SortedPairs ]] - [[ .Name ]]=[[ .Value ]] - [[ end ]][[ end ]] -
- Resolved - - [[ .Labels.alertname ]] -
- Go to alerts page -
-
-[[ end ]] - -
diff --git a/emails/templates/ng_alert_notification.mjml b/emails/templates/ng_alert_notification.mjml new file mode 100644 index 00000000000..f5ff61069c4 --- /dev/null +++ b/emails/templates/ng_alert_notification.mjml @@ -0,0 +1,122 @@ + + + + + {{ Subject .Subject "{{ .Title }}" }} + + + + + + + + + + + + + + + + {{ if .Message }} + + + + + + + {{ range $line := (splitList "\n" .Message) }} + + {{ $line }}
+ + {{ end }} + +
+
+
+
+ + + + {{ else }} + + + + + + + {{ if .Alerts.Firing }} + + + + + + +

🔥 {{ .Alerts.Firing | len }} firing instances

+
+
+
+ + + + {{ range .Alerts.Firing }} + + + + + + + + + + {{ end }} + + + + + {{ end }} + + + + + {{ if .Alerts.Resolved }} + + + + + +

✅ {{ .Alerts.Resolved | len }} resolved instances

+
+
+
+ + + + {{ range .Alerts.Resolved }} + + + + + + + + + + {{ end }} + + + + + {{ end }} + + + + + {{ end }} + + + + + +
+
diff --git a/emails/templates/partials/alerting/firing_instance.mjml b/emails/templates/partials/alerting/firing_instance.mjml new file mode 100644 index 00000000000..9f392b5e788 --- /dev/null +++ b/emails/templates/partials/alerting/firing_instance.mjml @@ -0,0 +1,23 @@ + + + + Firing + + + + + {{ .Labels.alertname }} + + + + {{ if gt (len .GeneratorURL) 0 }} + + + + View alert + + + + {{ end }} + + diff --git a/emails/templates/partials/alerting/grouping_labels.mjml b/emails/templates/partials/alerting/grouping_labels.mjml new file mode 100644 index 00000000000..629351abbb0 --- /dev/null +++ b/emails/templates/partials/alerting/grouping_labels.mjml @@ -0,0 +1,30 @@ + + + + + {{ if eq (.GroupLabels.SortedPairs.Names | join ",") "alertname,grafana_folder" }} + + +

📁 {{ .GroupLabels.grafana_folder }} › {{ .GroupLabels.alertname }}

+
+ + {{ else }} + + + +

+ 📁 Grouped by  +

+ + {{ range .GroupLabels.SortedPairs }} + + {{ .Name }}={{ .Value }} + + {{ end }} + +
+ + {{ end }} + +
+
diff --git a/emails/templates/partials/alerting/instance_details.mjml b/emails/templates/partials/alerting/instance_details.mjml new file mode 100644 index 00000000000..26a4bbd7856 --- /dev/null +++ b/emails/templates/partials/alerting/instance_details.mjml @@ -0,0 +1,195 @@ + + + {{ if .ImageURL }} + + + + + + + + {{ end }} + + + + + {{ if .EmbeddedImage }} + + + + + + + + {{ end }} + + + + + + + {{ if .Annotations.summary }} + + + Summary + + + {{- .Annotations.summary -}} + + + {{ end }} + + + + + {{ if .Annotations.description }} + + + Description + + + + {{ range $line := (splitList "\n" .Annotations.description) }} + + {{ $line }}
+ + {{ end }} + +
+ + {{ end }} + +
+
+ + + + {{ if .Values }} + + + + + Values + + + + + + + + {{ range $refID, $value := .Values }} + + {{ $refID }}={{ $value }}  + + {{ end }} + + + + + + {{ end }} + + + + + + + + {{ if .Labels.SortedPairs }} + + + Labels + + + + + {{ range .Labels.SortedPairs }} + + + + {{ .Name }} + + + {{ .Value }} + + + + {{ end }} + + + + + {{ end }} + + + + + {{ if .Annotations.SortedPairs }} + + + Annotations + + + + + {{ range .Annotations.SortedPairs }} + + + + {{ .Name }} + + + {{ .Value }} + + + + {{ end }} + + + + + {{ end }} + + + + + + + {{ if .SilenceURL }} + + + Silence + + + {{ end }} + {{ if .Annotations.runbook_url }} + + + View runbook + + + {{ end }} + {{ if .DashboardURL }} + + + View dashboard + + + {{ end }} + {{ if .PanelURL }} + + + View panel + + + {{ end }} + + + + + + + Observed {{ ago .StartsAt }} before this notification was delivered, at {{ .StartsAt }} + + + diff --git a/emails/templates/partials/alerting/resolved_instance.mjml b/emails/templates/partials/alerting/resolved_instance.mjml new file mode 100644 index 00000000000..5b9060f5922 --- /dev/null +++ b/emails/templates/partials/alerting/resolved_instance.mjml @@ -0,0 +1,23 @@ + + + + Resolved + + + + + {{ .Labels.alertname }} + + + + {{ if gt (len .GeneratorURL) 0 }} + + + + View alert + + + + {{ end }} + + diff --git a/emails/templates/partials/alerting/summary.mjml b/emails/templates/partials/alerting/summary.mjml new file mode 100644 index 00000000000..16d910fe437 --- /dev/null +++ b/emails/templates/partials/alerting/summary.mjml @@ -0,0 +1,28 @@ + + {{ $numberOfFiringInstance := (len .Alerts.Firing) }} + {{ $numberOfResolvedAlerts := (len .Alerts.Resolved) }} + + + + + {{ if $numberOfFiringInstance }} + + {{ $numberOfFiringInstance }} firing alert {{ $numberOfFiringInstance| plural "instance" "instances" }} + + {{ end }} + + + {{ if and $numberOfFiringInstance $numberOfResolvedAlerts }} + +  and  + + {{ end }} + + + {{ if $numberOfResolvedAlerts }} + + {{ $numberOfResolvedAlerts }} resolved alert {{ $numberOfResolvedAlerts| plural "instance" "instances" }} + + {{ end }} + + diff --git a/emails/templates/partials/layout/default.txt b/emails/templates/partials/layout/default.txt new file mode 100644 index 00000000000..a27a0817378 --- /dev/null +++ b/emails/templates/partials/layout/default.txt @@ -0,0 +1,3 @@ +{{> body }} + +Sent by Grafana v[[.BuildVersion]] (c) [[now | date "2006"]] Grafana Labs diff --git a/emails/templates/partials/layout/footer.mjml b/emails/templates/partials/layout/footer.mjml new file mode 100644 index 00000000000..fddf35962c8 --- /dev/null +++ b/emails/templates/partials/layout/footer.mjml @@ -0,0 +1,5 @@ + + + © {{ now | date "2006" }} Grafana Labs. Sent by Grafana v{{ .BuildVersion }}. + + diff --git a/emails/templates/partials/layout/head.mjml b/emails/templates/partials/layout/head.mjml new file mode 100644 index 00000000000..83c9269b784 --- /dev/null +++ b/emails/templates/partials/layout/head.mjml @@ -0,0 +1,10 @@ + + + + + + + a { + color: #6E9FFF; + } + diff --git a/emails/templates/partials/layout/header.mjml b/emails/templates/partials/layout/header.mjml new file mode 100644 index 00000000000..15d50e0024b --- /dev/null +++ b/emails/templates/partials/layout/header.mjml @@ -0,0 +1,3 @@ + + + diff --git a/emails/templates/reset_password.html b/emails/templates/reset_password.html deleted file mode 100644 index e9d1527116c..00000000000 --- a/emails/templates/reset_password.html +++ /dev/null @@ -1,42 +0,0 @@ -[[Subject .Subject "Reset your Grafana password - [[.Name]]"]] - - - - - -
- - - - - - -
-

Hi [[.Name]],

-
- -
- - - - - -
- - - - - -
-

- Please click the following link to reset your password within [[.EmailCodeValidHours]] hours. -

-

- [[.AppUrl]]user/password/reset?code=[[.Code]] -

-

Not working? Try copying and pasting it to your browser.

-
- -
- - diff --git a/emails/templates/reset_password.mjml b/emails/templates/reset_password.mjml new file mode 100644 index 00000000000..f1face73715 --- /dev/null +++ b/emails/templates/reset_password.mjml @@ -0,0 +1,36 @@ + + + + + {{ Subject .Subject "Reset your Grafana password - {{.Name}}" }} + + + + + + + + + + +

Hi {{ .Name }},

+
+ + Please click the following link to reset your password within {{ .EmailCodeValidHours }} hours. + + + Reset Password + + + You can also copy and paste this link into your browser directly: + + + {{ .AppUrl }}user/password/reset?code={{ .Code }} + +
+
+ + + +
+
diff --git a/emails/templates/signup_started.html b/emails/templates/signup_started.html deleted file mode 100644 index 3e8b3e0b976..00000000000 --- a/emails/templates/signup_started.html +++ /dev/null @@ -1,46 +0,0 @@ -[[Subject .Subject "Welcome to Grafana, please complete your sign up!"]] - - - - - -
- - - - - - -
-

Complete the signup

-
- -
- - - - - -
- - - - - - - - -
- Copy and paste the email verification code:
- [[.Code]]
in - the sign up form or use the link below. -
- - - - -
Complete Sign Up
-
-
- - diff --git a/emails/templates/signup_started.mjml b/emails/templates/signup_started.mjml new file mode 100644 index 00000000000..98b85a6e955 --- /dev/null +++ b/emails/templates/signup_started.mjml @@ -0,0 +1,39 @@ + + + + + {{ Subject .Subject "Welcome to Grafana, please complete your sign up!" }} + + + + + + + + + + +

Complete the signup

+
+ + Copy and paste the email verification code in the sign up form or use the link below. + + + {{ .Code }} + + + Complete Sign Up + + + You can also copy and paste this link into your browser directly: + + + {{ .SignUpUrl }} + +
+
+ + + +
+
diff --git a/emails/templates/welcome_on_signup.html b/emails/templates/welcome_on_signup.html deleted file mode 100644 index 7e2e004342a..00000000000 --- a/emails/templates/welcome_on_signup.html +++ /dev/null @@ -1,48 +0,0 @@ -[[Subject .Subject "Welcome to Grafana"]] - - - - - -
- - - - - - - - - -
-

Hi [[.Name]],

-
- Welcome! Ready to start building some beautiful metric and analytic dashboards? -
- -
- - - - - -
- - - - - - - - -
-

- If you are new to Grafana, refer to the Getting started with Grafana guide. -

-
- Thank you for joining our community. -
-

The Grafana Team

-
-
- diff --git a/emails/templates/welcome_on_signup.mjml b/emails/templates/welcome_on_signup.mjml new file mode 100644 index 00000000000..642496c3762 --- /dev/null +++ b/emails/templates/welcome_on_signup.mjml @@ -0,0 +1,40 @@ + + + + + {{ Subject .Subject "Welcome to Grafana" }} + + + + + + + + + + +

Hi {{ .Name }},

+
+ + Welcome! Ready to start building some beautiful metric and analytic dashboards? + + + If you are new to Grafana, refer to the Getting started with Grafana + guide. + + + Check out our getting started guide + + + Thank you for joining our community. + + + The Grafana Team + +
+
+ + + +
+
diff --git a/go.mod b/go.mod index 1e5d862639d..50dbe4cff3c 100644 --- a/go.mod +++ b/go.mod @@ -256,7 +256,6 @@ require ( github.com/grafana/codejen v0.0.3 github.com/grafana/dskit v0.0.0-20211011144203-3a88ec0b675f github.com/jmoiron/sqlx v1.3.5 - github.com/kr/pretty v0.3.0 github.com/matryer/is v1.4.0 github.com/parca-dev/parca v0.12.1 github.com/urfave/cli v1.22.9 @@ -271,6 +270,8 @@ require ( require ( cloud.google.com/go v0.102.0 // indirect github.com/Azure/azure-pipeline-go v0.2.3 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.1.1 // indirect github.com/armon/go-metrics v0.3.10 // indirect github.com/bmatcuk/doublestar v1.1.1 // indirect github.com/buildkite/yaml v2.1.0+incompatible // indirect @@ -287,15 +288,19 @@ require ( github.com/gosimple/unidecode v1.0.1 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/memberlist v0.4.0 // indirect + github.com/huandu/xstrings v1.3.1 // indirect github.com/invopop/yaml v0.1.0 // indirect github.com/kr/text v0.2.0 // indirect github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-ieproxy v0.0.3 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/mapstructure v1.4.3 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/rivo/uniseg v0.2.0 // indirect - github.com/rogpeppe/go-internal v1.8.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/segmentio/asm v1.1.4 // indirect + github.com/shopspring/decimal v1.2.0 // indirect + github.com/spf13/cast v1.3.1 // indirect go.starlark.net v0.0.0-20221020143700-22309ac47eac // indirect ) @@ -305,6 +310,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v0.22.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.2.1 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0 // indirect + github.com/Masterminds/sprig/v3 v3.2.2 github.com/Microsoft/go-winio v0.5.2 // indirect github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 // indirect github.com/RoaringBitmap/roaring v0.9.4 // indirect diff --git a/go.sum b/go.sum index 88e4e481756..e5f5e30bf67 100644 --- a/go.sum +++ b/go.sum @@ -234,11 +234,16 @@ github.com/HdrHistogram/hdrhistogram-go v1.0.1/go.mod h1:BWJ+nMSHY3L41Zj7CA3uXnl github.com/HdrHistogram/hdrhistogram-go v1.1.0/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Masterminds/sprig v2.16.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= +github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmyxvxX8= +github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= github.com/Masterminds/squirrel v0.0.0-20161115235646-20f192218cf5/go.mod h1:xnKTFzjGUiZtiOagBsfnvomW+nJg2usB1ZpordQWqNM= github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= @@ -1507,6 +1512,8 @@ github.com/hetznercloud/hcloud-go v1.33.2 h1:ptWKVYLW7YtjXzsqTFKFxwpVo3iM9UMkVPB github.com/hodgesds/perf-utils v0.0.8/go.mod h1:F6TfvsbtrF88i++hou29dTXlI2sfsJv+gRZDtmTJkAs= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo= +github.com/huandu/xstrings v1.3.1 h1:4jgBlKK6tLKFvO8u5pmYjG91cqytmDCDvGh7ECVFfFs= +github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/iancoleman/strcase v0.0.0-20180726023541-3605ed457bf7/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -1865,6 +1872,7 @@ github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= @@ -2265,6 +2273,7 @@ github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shirou/gopsutil v3.21.6+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 h1:bUGsEnyNbVPw06Bs80sCeARAlK8lhwqGyi6UT8ymuGk= github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= @@ -2305,6 +2314,7 @@ github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY52 github.com/spf13/afero v1.3.4/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= @@ -2667,6 +2677,7 @@ golang.org/x/crypto v0.0.0-20191202143827-86a70503ff7e/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200422194213-44a606286825/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= diff --git a/pkg/services/ngalert/notifier/channels/email_test.go b/pkg/services/ngalert/notifier/channels/email_test.go index da621f7e646..d29bc1273f0 100644 --- a/pkg/services/ngalert/notifier/channels/email_test.go +++ b/pkg/services/ngalert/notifier/channels/email_test.go @@ -189,9 +189,13 @@ func TestEmailNotifierIntegration(t *testing.T) { messageTmpl: "", expSubject: "[FIRING:2] ", expSnippets: []string{ - "Firing: 2 alerts", - "
  • alertname: FiringOne
  • severity: warning
  • ", - "
  • alertname: FiringTwo
  • severity: critical
  • ", + "2 firing instances", + "severity", + "warning\n", + "critical\n", + "alertname", + "FiringTwo\n", + "FiringOne\n", " - + + + - - - - - + + {{ Subject .Subject "{{ .InvitedBy }} has added you to the {{ .OrgName }} organization" }} + + + + + + + + + + + + + + + + + + - - - - -
    - - - - - - - - -
    -

    {{.InvitedBy}} has added you to the {{.OrgName}} organization in Grafana. -

    Once logged in, {{.OrgName}} will be available in the left side menu, in the dropdown below your username.

    -
    - - - - -
    Log in now
    -
    -
    - - - - - - - - - - - - - - - - - + +
    + +
    + + + + + + +
    + +
    + + + + + + +
    + + + + + + +
    + +
    +
    +
    + +
    +
    + +
    + + + + + + +
    + +
    + + + + + + + + + + + + + + + + + + + + + +
    +
    +

    You have been added to {{ .OrgName }}

    + {{ .InvitedBy }} has added you to the {{ .OrgName }} organization in Grafana. +
    +
    +
    Once logged in, {{ .OrgName }} will be available to switch to in the user interface.
    +
    +
    Log in now by clicking the link below:
    +
    + + + + + + +
    + Login to Grafana +
    +
    +
    You can also copy and paste this link into your browser directly:
    +
    + +
    +
    + +
    +
    + +
    + + + + + + +
    + +
    + + + + + + +
    +
    © {{ now | date "2006" }} Grafana Labs. Sent by Grafana v{{ .BuildVersion }}.
    +
    +
    + +
    +
    + +
    + diff --git a/public/emails/invited_to_org.txt b/public/emails/invited_to_org.txt index 6d4a8a3e74b..61982198bef 100644 --- a/public/emails/invited_to_org.txt +++ b/public/emails/invited_to_org.txt @@ -3,10 +3,10 @@ You have been added to {{.OrgName}} {{.InvitedBy}} has added you to the {{.OrgName}} organization in Grafana. -Once logged in, {{.OrgName}} will be available in the left side menu, in the dropdown -below your username. +Once logged in, {{.OrgName}} will be available in the left side menu, in the dropdown below your username. Log in now: {{.AppUrl}} -Sent by Grafana v{{.BuildVersion}} (c) 2022 Grafana Labs + +Sent by Grafana v{{.BuildVersion}} (c) {{now | date "2006"}} Grafana Labs diff --git a/public/emails/new_user_invite.html b/public/emails/new_user_invite.html index 0728cefffdc..dd70e3d50f7 100644 --- a/public/emails/new_user_invite.html +++ b/public/emails/new_user_invite.html @@ -1,286 +1,217 @@ - - + + + - - - - - + + {{ Subject .Subject "{{ .InvitedBy }} has invited you to join Grafana" }} + + + + + + + + + + + + + + + + + + - - - - -
    - - - - - - - - - - - -
    -

    You've been invited to join the {{.OrgName}} organization by {{.InvitedBy}}. To accept your invitation and join the team, please click the link below:

    -
    - - - - -
    Accept Invitation
    -
    -

    You can also copy and paste this link into your browser directly: {{.LinkUrl}}

    -
    -
    - - - - - - - - - - - - - - - + +
    + +
    + + + + + + +
    + +
    + + + + + + +
    + + + + + + +
    + +
    +
    +
    + +
    +
    + +
    + + + + + + +
    + +
    + + + + + + + + + + + + + + + + + + +
    +
    +

    You're invited to join {{ .OrgName }}

    +
    +
    +
    You've been invited to join the {{ .OrgName }} organization by {{ .InvitedBy }}. To accept your invitation and join the team, please click the link below:
    +
    + + + + + + +
    + Accept Invitation +
    +
    +
    You can also copy and paste this link into your browser directly:
    +
    + +
    +
    + +
    +
    + +
    + + + + + + +
    + +
    + + + + + + +
    +
    © {{ now | date "2006" }} Grafana Labs. Sent by Grafana v{{ .BuildVersion }}.
    +
    +
    + +
    +
    + +
    + diff --git a/public/emails/new_user_invite.txt b/public/emails/new_user_invite.txt index b9b686b8839..2cc475a1052 100644 --- a/public/emails/new_user_invite.txt +++ b/public/emails/new_user_invite.txt @@ -2,10 +2,8 @@ You're invited to join {{.OrgName}} -You've been invited to join the {{.OrgName}} organization by {{.InvitedBy}}. To accept -your invitation and join the team, copy and paste the link below into your browser -directly: +You've been invited to join the {{.OrgName}} organization by {{.InvitedBy}}. To accept your invitation and join the team, copy and paste the link below into your browser directly: {{.LinkUrl}} -Sent by Grafana v{{.BuildVersion}} (c) 2022 Grafana Labs +Sent by Grafana v{{.BuildVersion}} (c) {{now | date "2006"}} Grafana Labs diff --git a/public/emails/ng_alert_notification.html b/public/emails/ng_alert_notification.html index c0824fac335..d4858b482f9 100644 --- a/public/emails/ng_alert_notification.html +++ b/public/emails/ng_alert_notification.html @@ -1,386 +1,1315 @@ - - + + + - - - - - + + {{ Subject .Subject "{{ .Title }}" }} + + + + + + + + + + + + + + + + + + {{ $numberOfFiringInstance := (len .Alerts.Firing) }} + {{ $numberOfResolvedAlerts := (len .Alerts.Resolved) }} + + + +
    + + {{ if $numberOfFiringInstance }} + + {{ $numberOfFiringInstance }} firing alert {{ $numberOfFiringInstance| plural "instance" "instances" }} + {{ end }} - {{ if .Annotations.runbook_url }} - - - View Runbook - + + + {{ if and $numberOfFiringInstance $numberOfResolvedAlerts }} +  and  {{ end }} - {{ if .DashboardURL}} - - - Go to Dashboard - + + + {{ if $numberOfResolvedAlerts }} + + {{ $numberOfResolvedAlerts }} resolved alert {{ $numberOfResolvedAlerts| plural "instance" "instances" }} + {{ end }} - {{ if .PanelURL}} - - - Go to Panel - - {{ end }} - {{ if gt (len .GeneratorURL) 0 }}Source{{ end }} - - - - -
    -
    -
    - - -{{ end }} - -{{ if gt (len .Message) 0 }} -
    {{ .Message }} -{{ else }} - - - - - - - -
    - - {{ if gt (len .Alerts.Firing) 0 }} - - - - {{ range .Alerts.Firing }} - - - - - {{ template "alert" . }} - {{ end }} - {{ end }} - {{ if gt (len .Alerts.Resolved) 0 }} - - - - {{ range .Alerts.Resolved }} - - - - - {{ template "alert" . }} - {{ end }} - {{ end }} - - - -
    - Firing: {{ .Alerts.Firing | len }} alert{{ if gt (len .Alerts.Firing) 1 }}s{{ end }}{{ if gt (len .GroupLabels.SortedPairs) 1 }} for - {{ range .GroupLabels.SortedPairs }} - {{ .Name }}={{ .Value }} - {{ end }}{{ end }} -
    - Firing - - {{ .Labels.alertname }} -
    - Resolved: {{ .Alerts.Resolved | len }} alert{{ if gt (len .Alerts.Resolved) 1 }}s{{ end }}{{ if gt (len .GroupLabels.SortedPairs) 1 }} for - {{ range .GroupLabels.SortedPairs }} - {{ .Name }}={{ .Value }} - {{ end }}{{ end }} -
    - Resolved - - {{ .Labels.alertname }} -
    - Go to alerts page + + +
    + +
    + + + + +
    + +
    + + + + + + +
    + + + + + + +
    + +
    +
    +
    +
    -
    -{{ end }} - - - - -
    - - - - - - - - - - +
    + +
    + + + + + + +
    + +
    + + + {{ if eq (.GroupLabels.SortedPairs.Names | join ",") "alertname,grafana_folder" }} + + + + {{ else }} + + + + {{ end }} + +
    +
    +

    📁 {{ .GroupLabels.grafana_folder }} › {{ .GroupLabels.alertname }}

    +
    +
    +
    +

    📁 Grouped by 

    + + {{ range .GroupLabels.SortedPairs }} + + {{ .Name }}={{ .Value }} + + {{ end }} + +
    +
    +
    + +
    +
    + + {{ if .Message }} + +
    + + + + + + +
    + +
    + + + + + + +
    + +
    + + + + + + +
    +
    + + {{ range $line := (splitList "\n" .Message) }} + + {{ $line }}
    + + {{ end }} + +
    +
    +
    + +
    +
    + +
    +
    + + {{ else }}{{ if .Alerts.Firing }} + +
    + + + + + + +
    + +
    + + + + + + +
    +
    +

    🔥 {{ .Alerts.Firing | len }} firing instances

    +
    +
    +
    + +
    +
    + + {{ range .Alerts.Firing }} + +
    + + + + + + +
    + +
    + + + + + + +
    + +
    + + + + + + +
    + + + + + + +
    + Firing +
    +
    +
    + +
    + + + + + + +
    +
    {{ .Labels.alertname }}
    +
    +
    + + {{ if gt (len .GeneratorURL) 0 }} + +
    + + + + + + +
    + + + + + + +
    + View alert +
    +
    +
    + + {{ end }} + +
    +
    + + {{ if .ImageURL }} + +
    + + + + + + +
    + +
    + + + + + + +
    + + + + + + +
    + + + +
    +
    +
    + +
    +
    + + {{ end }}{{ if .EmbeddedImage }} + +
    + + + + + + +
    + +
    + + + + + + +
    + + + + + + +
    + +
    +
    +
    + +
    +
    + + {{ end }} + +
    + + + + + + +
    + +
    + + + {{ if .Annotations.summary }} + + + + + + + {{ end }}{{ if .Annotations.description }} + + + + + + + {{ end }} + +
    +
    Summary
    +
    +
    {{- .Annotations.summary -}}
    +
    +
    Description
    +
    +
    + + {{ range $line := (splitList "\n" .Annotations.description) }} + + {{ $line }}
    + + {{ end }} + +
    +
    +
    + +
    +
    + + {{ if .Values }} + +
    + + + + + + +
    + +
    + + + + + + +
    +
    Values
    +
    +
    + +
    +
    + +
    + + + + + + +
    + +
    + + + + + + +
    + + + + + + +
    +
    + + {{ range $refID, $value := .Values }} + + {{ $refID }}={{ $value }}  + {{ end }} + +
    +
    +
    +
    + +
    +
    + + {{ end }} + +
    + + + + + + +
    + +
    + + + {{ if .Labels.SortedPairs }} + + + + + + + {{ end }}{{ if .Annotations.SortedPairs }} + + + + + + + {{ end }} + +
    +
    Labels
    +
    + + + {{ range .Labels.SortedPairs }} + + + + + + + {{ end }} + +
    + {{ .Name }} + + {{ .Value }} +
    +
    +
    Annotations
    +
    + + + {{ range .Annotations.SortedPairs }} + + + + + + + {{ end }} + +
    + {{ .Name }} + + {{ .Value }} +
    +
    +
    + +
    +
    + +
    + + + + + + +
    + + {{ if .SilenceURL }} + +
    + + + + + + +
    + + + + + + +
    + Silence +
    +
    +
    + + {{ end }}{{ if .Annotations.runbook_url }} + +
    + + + + + + +
    + + + + + + +
    + View runbook +
    +
    +
    + + {{ end }}{{ if .DashboardURL }} + +
    + + + + + + +
    + + + + + + +
    + View dashboard +
    +
    +
    + + {{ end }}{{ if .PanelURL }} + +
    + + + + + + +
    + + + + + + +
    + View panel +
    +
    +
    + + {{ end }} + +
    +
    + +
    + + + + + + +
    + +
    + + + + + + +
    +
    Observed {{ ago .StartsAt }} before this notification was delivered, at {{ .StartsAt }}
    +
    +
    + +
    +
    + +
    +
    + +
    + + + + + + +
    + +
    +
    + + {{ end }}{{ end }}{{ if .Alerts.Resolved }} + +
    + + + + + + +
    + +
    + + + + + + +
    +
    +

    ✅ {{ .Alerts.Resolved | len }} resolved instances

    +
    +
    +
    + +
    +
    + + {{ range .Alerts.Resolved }} + +
    + + + + + + +
    + +
    + + + + + + +
    + +
    + + + + + + +
    + + + + + + +
    +

    Resolved

    +
    +
    +
    + +
    + + + + + + +
    +
    {{ .Labels.alertname }}
    +
    +
    + + {{ if gt (len .GeneratorURL) 0 }} + +
    + + + + + + +
    + + + + + + +
    + View alert +
    +
    +
    + + {{ end }} + +
    +
    + + {{ if .ImageURL }} + +
    + + + + + + +
    + +
    + + + + + + +
    + + + + + + +
    + + + +
    +
    +
    + +
    +
    + + {{ end }}{{ if .EmbeddedImage }} + +
    + + + + + + +
    + +
    + + + + + + +
    + + + + + + +
    + +
    +
    +
    + +
    +
    + + {{ end }} + +
    + + + + + + +
    + +
    + + + {{ if .Annotations.summary }} + + + + + + + {{ end }}{{ if .Annotations.description }} + + + + + + + {{ end }} + +
    +
    Summary
    +
    +
    {{- .Annotations.summary -}}
    +
    +
    Description
    +
    +
    + + {{ range $line := (splitList "\n" .Annotations.description) }} + + {{ $line }}
    + + {{ end }} + +
    +
    +
    + +
    +
    + + {{ if .Values }} + +
    + + + + + + +
    + +
    + + + + + + +
    +
    Values
    +
    +
    + +
    +
    + +
    + + + + + + +
    + +
    + + + + + + +
    + + + + + + +
    +
    + + {{ range $refID, $value := .Values }} + + {{ $refID }}={{ $value }}  + {{ end }} + +
    +
    +
    +
    + +
    +
    + + {{ end }} + +
    + + + + + + +
    + +
    + + + {{ if .Labels.SortedPairs }} + + + + + + + {{ end }}{{ if .Annotations.SortedPairs }} + + + + + + + {{ end }} + +
    +
    Labels
    +
    + + + {{ range .Labels.SortedPairs }} + + + + + + + {{ end }} + +
    + {{ .Name }} + + {{ .Value }} +
    +
    +
    Annotations
    +
    + + + {{ range .Annotations.SortedPairs }} + + + + + + + {{ end }} + +
    + {{ .Name }} + + {{ .Value }} +
    +
    +
    + +
    +
    + +
    + + + + + + +
    + + {{ if .SilenceURL }} + +
    + + + + + + +
    + + + + + + +
    + Silence +
    +
    +
    + + {{ end }}{{ if .Annotations.runbook_url }} + +
    + + + + + + +
    + + + + + + +
    + View runbook +
    +
    +
    + + {{ end }}{{ if .DashboardURL }} + +
    + + + + + + +
    + + + + + + +
    + View dashboard +
    +
    +
    + + {{ end }}{{ if .PanelURL }} + +
    + + + + + + +
    + + + + + + +
    + View panel +
    +
    +
    + + {{ end }} + +
    +
    + +
    + + + + + + +
    + +
    + + + + + + +
    +
    Observed {{ ago .StartsAt }} before this notification was delivered, at {{ .StartsAt }}
    +
    +
    + +
    +
    + +
    +
    + +
    + + + + + + +
    + +
    +
    + + {{ end }}{{ end }}{{ end }} + +
    + + + + + + +
    + +
    + + + + + + +
    +
    © {{ now | date "2006" }} Grafana Labs. Sent by Grafana v{{ .BuildVersion }}.
    +
    +
    + +
    +
    + +
    + diff --git a/public/emails/ng_alert_notification.txt b/public/emails/ng_alert_notification.txt index 485bdfe3f07..be157396c17 100644 --- a/public/emails/ng_alert_notification.txt +++ b/public/emails/ng_alert_notification.txt @@ -19,8 +19,7 @@ Annotations: {{ range .Annotations.SortedPairs }} {{ .Name }} = {{ .Value }} {{ end }} -{{ end }}{{ if gt (len .Alerts.Resolved) 0 }}({{ .Alerts.Resolved | len }}) Resolved{{ end -}} +{{ end }}{{ if gt (len .Alerts.Resolved) 0 }}({{ .Alerts.Resolved | len }}) Resolved{{ end }} {{ range .Alerts.Resolved }} Labels: {{ range .Labels.SortedPairs }} @@ -38,4 +37,5 @@ Annotations: Go to the Alerts page: {{.AlertPageUrl}} -Sent by Grafana v{{.BuildVersion}} (c) 2022 Grafana Labs + +Sent by Grafana v{{.BuildVersion}} (c) {{now | date "2006"}} Grafana Labs diff --git a/public/emails/reset_password.html b/public/emails/reset_password.html index ab4ef1c761c..b064b21c180 100644 --- a/public/emails/reset_password.html +++ b/public/emails/reset_password.html @@ -1,279 +1,217 @@ - - + + + - - - - - + + {{ Subject .Subject "Reset your Grafana password - {{.Name}}" }} + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - + +
    + +
    + + + + + + +
    + +
    + + + + + + +
    + + + + + + +
    + +
    +
    +
    + +
    +
    + +
    + + + + + + +
    + +
    + + + + + + + + + + + + + + + + + + +
    +
    +

    Hi {{ .Name }},

    +
    +
    +
    Please click the following link to reset your password within {{ .EmailCodeValidHours }} hours.
    +
    + + + + + + +
    + Reset Password +
    +
    +
    You can also copy and paste this link into your browser directly:
    +
    + +
    +
    + +
    +
    + +
    + + + + + + +
    + +
    + + + + + + +
    +
    © {{ now | date "2006" }} Grafana Labs. Sent by Grafana v{{ .BuildVersion }}.
    +
    +
    + +
    +
    + +
    + diff --git a/public/emails/reset_password.txt b/public/emails/reset_password.txt index 0c5397416fc..0b15365f391 100644 --- a/public/emails/reset_password.txt +++ b/public/emails/reset_password.txt @@ -2,8 +2,8 @@ Hi {{.Name}}, -Copy and paste the following link directly in your browser to reset your password within -{{.EmailCodeValidHours}} hours. +Copy and paste the following link directly in your browser to reset your password within {{.EmailCodeValidHours}} hours. {{.AppUrl}}user/password/reset?code={{.Code}} -Sent by Grafana v{{.BuildVersion}} (c) 2022 Grafana Labs + +Sent by Grafana v{{.BuildVersion}} (c) {{now | date "2006"}} Grafana Labs diff --git a/public/emails/signup_started.html b/public/emails/signup_started.html index 15f1945862d..48d4c6236df 100644 --- a/public/emails/signup_started.html +++ b/public/emails/signup_started.html @@ -1,283 +1,232 @@ - - + + + - - - - - + + {{ Subject .Subject "Welcome to Grafana, please complete your sign up!" }} + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - + +
    + +
    + + + + + + +
    + +
    + + + + + + +
    + + + + + + +
    + +
    +
    +
    + +
    +
    + +
    + + + + + + +
    + +
    + + + + + + + + + + + + + + + + + + + + + +
    +
    +

    Complete the signup

    +
    +
    +
    Copy and paste the email verification code in the sign up form or use the link below.
    +
    + + + + + + +
    +

    + {{ .Code }} +

    +
    +
    + + + + + + +
    + Complete Sign Up +
    +
    +
    You can also copy and paste this link into your browser directly:
    +
    + +
    +
    + +
    +
    + +
    + + + + + + +
    + +
    + + + + + + +
    +
    © {{ now | date "2006" }} Grafana Labs. Sent by Grafana v{{ .BuildVersion }}.
    +
    +
    + +
    +
    + +
    + diff --git a/public/emails/signup_started.txt b/public/emails/signup_started.txt index 94dd8547936..a40e7403ecd 100644 --- a/public/emails/signup_started.txt +++ b/public/emails/signup_started.txt @@ -8,4 +8,5 @@ in the sign up form or use the link below. {{.SignUpUrl}} -Sent by Grafana v{{.BuildVersion}} (c) 2022 Grafana Labs + +Sent by Grafana v{{.BuildVersion}} (c) {{now | date "2006"}} Grafana Labs diff --git a/public/emails/welcome_on_signup.html b/public/emails/welcome_on_signup.html index 28ac926fb06..a532fc8a58a 100644 --- a/public/emails/welcome_on_signup.html +++ b/public/emails/welcome_on_signup.html @@ -1,285 +1,222 @@ - - + + + - - - - - + + {{ Subject .Subject "Welcome to Grafana" }} + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - + +
    + +
    + + + + + + +
    + +
    + + + + + + +
    + + + + + + +
    + +
    +
    +
    + +
    +
    + +
    + + + + + + +
    + +
    + + + + + + + + + + + + + + + + + + + + + +
    +
    +

    Hi {{ .Name }},

    +
    +
    +
    Welcome! Ready to start building some beautiful metric and analytic dashboards?
    +
    +
    If you are new to Grafana, refer to the Getting started with Grafana guide.
    +
    + + + + + + +
    + Check out our getting started guide +
    +
    +
    Thank you for joining our community.
    +
    +
    The Grafana Team
    +
    +
    + +
    +
    + +
    + + + + + + +
    + +
    + + + + + + +
    +
    © {{ now | date "2006" }} Grafana Labs. Sent by Grafana v{{ .BuildVersion }}.
    +
    +
    + +
    +
    + +
    + diff --git a/public/emails/welcome_on_signup.txt b/public/emails/welcome_on_signup.txt index 31bd3632997..4608fa9e82e 100644 --- a/public/emails/welcome_on_signup.txt +++ b/public/emails/welcome_on_signup.txt @@ -4,11 +4,11 @@ Hi {{.Name}}, Welcome! Ready to start building some beautiful metric and analytic dashboards? -If you are new to Grafana, refer to the Getting started with Grafana guide on -https://grafana.com/docs/grafana/latest/getting-started/getting-started/. +If you are new to Grafana, refer to the Getting started with Grafana guide on https://grafana.com/docs/grafana/latest/getting-started/getting-started/. Thank you for joining our community. The Grafana team -Sent by Grafana v{{.BuildVersion}} (c) 2022 Grafana Labs + +Sent by Grafana v{{.BuildVersion}} (c) {{now | date "2006"}} Grafana Labs From fef1e1d5bc8c1ea7d6deb6a8ef808988cdec61aa Mon Sep 17 00:00:00 2001 From: Karl Persson Date: Fri, 18 Nov 2022 09:56:06 +0100 Subject: [PATCH 002/153] Auth: Refactor auth package (#58920) * Auth: move interface to its own file * Auth: move to test package * Auth: move quota consts to auth file * Auth: move service to impl package * Auth: move interfaces and related models to auth package * Auth: Create sub package and type alias to avoid circular dependency --- pkg/api/admin_users.go | 7 +-- pkg/api/admin_users_test.go | 13 ++--- pkg/api/common_test.go | 6 +-- pkg/api/http_server.go | 5 +- pkg/api/ldap_debug_test.go | 4 +- pkg/api/login.go | 7 +-- pkg/api/login_test.go | 12 ++--- pkg/api/user_token.go | 13 ++--- pkg/api/user_token_test.go | 41 +++++++------- pkg/cmd/grafana-cli/runner/wire.go | 5 +- pkg/cmd/grafana-cli/runner/wireexts_oss.go | 7 +-- pkg/middleware/auth.go | 5 +- pkg/middleware/middleware_test.go | 39 +++++++------- pkg/middleware/org_redirect_test.go | 10 ++-- pkg/middleware/quota_test.go | 6 +-- pkg/middleware/recovery_test.go | 4 +- pkg/middleware/testing.go | 6 +-- pkg/models/context.go | 3 +- pkg/models/usertoken/user_token.go | 26 +++++++++ .../backgroundsvcs/background_services.go | 4 +- pkg/server/wire.go | 5 +- pkg/server/wireexts_oss.go | 7 +-- pkg/services/accesscontrol/middleware.go | 6 +-- .../user_token.go => services/auth/auth.go} | 40 ++++++-------- .../auth/{ => authimpl}/auth_token.go | 54 +++++++++---------- .../auth/{ => authimpl}/auth_token_test.go | 26 ++++----- pkg/services/auth/{ => authimpl}/model.go | 16 ++---- .../auth/{ => authimpl}/token_cleanup.go | 2 +- .../auth/{ => authimpl}/token_cleanup_test.go | 2 +- pkg/services/auth/{ => authtest}/testing.go | 47 ++++++++-------- .../contexthandler/auth_proxy_test.go | 4 +- pkg/services/contexthandler/contexthandler.go | 10 ++-- .../contexthandler/contexthandler_test.go | 18 ++++--- pkg/services/ngalert/api/util_test.go | 3 +- pkg/services/quota/quotaimpl/quota_test.go | 3 +- 35 files changed, 245 insertions(+), 221 deletions(-) create mode 100644 pkg/models/usertoken/user_token.go rename pkg/{models/user_token.go => services/auth/auth.go} (74%) rename pkg/services/auth/{ => authimpl}/auth_token.go (91%) rename pkg/services/auth/{ => authimpl}/auth_token_test.go (96%) rename pkg/services/auth/{ => authimpl}/model.go (77%) rename pkg/services/auth/{ => authimpl}/token_cleanup.go (99%) rename pkg/services/auth/{ => authimpl}/token_cleanup_test.go (99%) rename pkg/services/auth/{ => authtest}/testing.go (77%) diff --git a/pkg/api/admin_users.go b/pkg/api/admin_users.go index e0b244bfab4..bf284cddb33 100644 --- a/pkg/api/admin_users.go +++ b/pkg/api/admin_users.go @@ -14,6 +14,7 @@ import ( "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/web" @@ -416,7 +417,7 @@ func (hs *HTTPServer) AdminGetUserAuthTokens(c *models.ReqContext) response.Resp // 404: notFoundError // 500: internalServerError func (hs *HTTPServer) AdminRevokeUserAuthToken(c *models.ReqContext) response.Response { - cmd := models.RevokeAuthTokenCmd{} + cmd := auth.RevokeAuthTokenCmd{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) } @@ -476,7 +477,7 @@ type AdminLogoutUserParams struct { type AdminRevokeUserAuthTokenParams struct { // in:body // required:true - Body models.RevokeAuthTokenCmd `json:"body"` + Body auth.RevokeAuthTokenCmd `json:"body"` // in:path // required:true UserID int64 `json:"user_id"` @@ -508,5 +509,5 @@ type AdminCreateUserResponseResponse struct { // swagger:response adminGetUserAuthTokensResponse type AdminGetUserAuthTokensResponse struct { // in:body - Body []*models.UserToken `json:"body"` + Body []*auth.UserToken `json:"body"` } diff --git a/pkg/api/admin_users_test.go b/pkg/api/admin_users_test.go index 41f79831f74..3de90077df9 100644 --- a/pkg/api/admin_users_test.go +++ b/pkg/api/admin_users_test.go @@ -13,6 +13,7 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/auth" + "github.com/grafana/grafana/pkg/services/auth/authtest" "github.com/grafana/grafana/pkg/services/login/loginservice" "github.com/grafana/grafana/pkg/services/login/logintest" "github.com/grafana/grafana/pkg/services/org" @@ -65,7 +66,7 @@ func TestAdminAPIEndpoint(t *testing.T) { }) t.Run("When a server admin attempts to revoke an auth token for a non-existing user", func(t *testing.T) { - cmd := models.RevokeAuthTokenCmd{AuthTokenId: 2} + cmd := auth.RevokeAuthTokenCmd{AuthTokenId: 2} mockUser := usertest.NewUserServiceFake() mockUser.ExpectedError = user.ErrUserNotFound adminRevokeUserAuthTokenScenario(t, "Should return not found when calling POST on", @@ -263,7 +264,7 @@ func putAdminScenario(t *testing.T, desc string, url string, routePattern string func adminLogoutUserScenario(t *testing.T, desc string, url string, routePattern string, fn scenarioFunc, userService *usertest.FakeUserService) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { hs := HTTPServer{ - AuthTokenService: auth.NewFakeUserAuthTokenService(), + AuthTokenService: authtest.NewFakeUserAuthTokenService(), userService: userService, } @@ -285,9 +286,9 @@ func adminLogoutUserScenario(t *testing.T, desc string, url string, routePattern }) } -func adminRevokeUserAuthTokenScenario(t *testing.T, desc string, url string, routePattern string, cmd models.RevokeAuthTokenCmd, fn scenarioFunc, userService user.Service) { +func adminRevokeUserAuthTokenScenario(t *testing.T, desc string, url string, routePattern string, cmd auth.RevokeAuthTokenCmd, fn scenarioFunc, userService user.Service) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { - fakeAuthTokenService := auth.NewFakeUserAuthTokenService() + fakeAuthTokenService := authtest.NewFakeUserAuthTokenService() hs := HTTPServer{ AuthTokenService: fakeAuthTokenService, @@ -315,7 +316,7 @@ func adminRevokeUserAuthTokenScenario(t *testing.T, desc string, url string, rou func adminGetUserAuthTokensScenario(t *testing.T, desc string, url string, routePattern string, fn scenarioFunc, userService *usertest.FakeUserService) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { - fakeAuthTokenService := auth.NewFakeUserAuthTokenService() + fakeAuthTokenService := authtest.NewFakeUserAuthTokenService() hs := HTTPServer{ AuthTokenService: fakeAuthTokenService, @@ -341,7 +342,7 @@ func adminGetUserAuthTokensScenario(t *testing.T, desc string, url string, route func adminDisableUserScenario(t *testing.T, desc string, action string, url string, routePattern string, fn scenarioFunc) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { - fakeAuthTokenService := auth.NewFakeUserAuthTokenService() + fakeAuthTokenService := authtest.NewFakeUserAuthTokenService() authInfoService := &logintest.AuthInfoServiceFake{} diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index e6f0c159401..39be06adbc5 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -28,7 +28,7 @@ import ( accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol" "github.com/grafana/grafana/pkg/services/annotations/annotationstest" - "github.com/grafana/grafana/pkg/services/auth" + "github.com/grafana/grafana/pkg/services/auth/authtest" "github.com/grafana/grafana/pkg/services/contexthandler" "github.com/grafana/grafana/pkg/services/contexthandler/authproxy" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" @@ -181,7 +181,7 @@ type scenarioContext struct { defaultHandler web.Handler req *http.Request url string - userAuthTokenService *auth.FakeUserAuthTokenService + userAuthTokenService *authtest.FakeUserAuthTokenService sqlStore sqlstore.Store authInfoService *logintest.AuthInfoServiceFake dashboardVersionService dashver.Service @@ -207,7 +207,7 @@ func getContextHandler(t *testing.T, cfg *setting.Cfg) *contexthandler.ContextHa cfg.RemoteCacheOptions = &setting.RemoteCacheOptions{ Name: "database", } - userAuthTokenSvc := auth.NewFakeUserAuthTokenService() + userAuthTokenSvc := authtest.NewFakeUserAuthTokenService() renderSvc := &fakeRenderService{} authJWTSvc := models.NewFakeJWTService() tracer := tracing.InitializeTracerForTest() diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index f2cfce24fc1..7320b71b2cd 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -15,6 +15,7 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/middleware/csrf" + "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/oauthtoken" "github.com/grafana/grafana/pkg/services/querylibrary" @@ -120,7 +121,7 @@ type HTTPServer struct { navTreeService navtree.Service CacheService *localcache.CacheService DataSourceCache datasources.CacheService - AuthTokenService models.UserTokenService + AuthTokenService auth.UserTokenService QuotaService quota.Service RemoteCacheService *remotecache.RemoteCache ProvisioningService provisioning.ProvisioningService @@ -220,7 +221,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi pluginRequestValidator models.PluginRequestValidator, pluginStaticRouteResolver plugins.StaticRouteResolver, pluginDashboardService plugindashboards.Service, pluginStore plugins.Store, pluginClient plugins.Client, pluginErrorResolver plugins.ErrorResolver, pluginInstaller plugins.Installer, settingsProvider setting.Provider, - dataSourceCache datasources.CacheService, userTokenService models.UserTokenService, + dataSourceCache datasources.CacheService, userTokenService auth.UserTokenService, cleanUpService *cleanup.CleanUpService, shortURLService shorturls.Service, queryHistoryService queryhistory.Service, correlationsService correlations.Service, thumbService thumbs.Service, remoteCache *remotecache.RemoteCache, provisioningService provisioning.ProvisioningService, loginService login.Service, authenticator loginpkg.Authenticator, accessControl accesscontrol.AccessControl, diff --git a/pkg/api/ldap_debug_test.go b/pkg/api/ldap_debug_test.go index 5a93f53e3ba..95b92e4e06f 100644 --- a/pkg/api/ldap_debug_test.go +++ b/pkg/api/ldap_debug_test.go @@ -15,7 +15,7 @@ import ( "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/auth" + "github.com/grafana/grafana/pkg/services/auth/authtest" "github.com/grafana/grafana/pkg/services/ldap" "github.com/grafana/grafana/pkg/services/login/loginservice" "github.com/grafana/grafana/pkg/services/login/logintest" @@ -379,7 +379,7 @@ func postSyncUserWithLDAPContext(t *testing.T, requestURL string, preHook func(* hs := &HTTPServer{ Cfg: sc.cfg, - AuthTokenService: auth.NewFakeUserAuthTokenService(), + AuthTokenService: authtest.NewFakeUserAuthTokenService(), Login: loginservice.LoginServiceMock{}, authInfoService: sc.authInfoService, userService: userService, diff --git a/pkg/api/login.go b/pkg/api/login.go index 20baa31bde5..9b23ad05b6e 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -16,6 +16,7 @@ import ( "github.com/grafana/grafana/pkg/login" "github.com/grafana/grafana/pkg/middleware/cookies" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/auth" loginService "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/user" @@ -227,7 +228,7 @@ func (hs *HTTPServer) LoginPost(c *models.ReqContext) response.Response { err = hs.loginUserWithUser(usr, c) if err != nil { - var createTokenErr *models.CreateTokenErr + var createTokenErr *auth.CreateTokenErr if errors.As(err, &createTokenErr) { resp = response.Error(createTokenErr.StatusCode, createTokenErr.ExternalErr, createTokenErr.InternalErr) } else { @@ -299,7 +300,7 @@ func (hs *HTTPServer) Logout(c *models.ReqContext) { } err := hs.AuthTokenService.RevokeToken(c.Req.Context(), c.UserToken, false) - if err != nil && !errors.Is(err, models.ErrUserTokenNotFound) { + if err != nil && !errors.Is(err, auth.ErrUserTokenNotFound) { hs.log.Error("failed to revoke auth token", "error", err) } @@ -370,7 +371,7 @@ func (hs *HTTPServer) samlSingleLogoutEnabled() bool { } func getLoginExternalError(err error) string { - var createTokenErr *models.CreateTokenErr + var createTokenErr *auth.CreateTokenErr if errors.As(err, &createTokenErr) { return createTokenErr.ExternalErr } diff --git a/pkg/api/login_test.go b/pkg/api/login_test.go index 27452b9bd09..ea30addb451 100644 --- a/pkg/api/login_test.go +++ b/pkg/api/login_test.go @@ -12,8 +12,6 @@ import ( "strings" "testing" - loginservice "github.com/grafana/grafana/pkg/services/login" - "github.com/grafana/grafana/pkg/services/navtree" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -25,9 +23,11 @@ import ( "github.com/grafana/grafana/pkg/login" "github.com/grafana/grafana/pkg/login/social" "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/auth" + "github.com/grafana/grafana/pkg/services/auth/authtest" "github.com/grafana/grafana/pkg/services/hooks" "github.com/grafana/grafana/pkg/services/licensing" + loginservice "github.com/grafana/grafana/pkg/services/login" + "github.com/grafana/grafana/pkg/services/navtree" "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/secrets/fakes" secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager" @@ -323,7 +323,7 @@ func TestLoginPostRedirect(t *testing.T) { Cfg: setting.NewCfg(), HooksService: &hooks.HooksService{}, License: &licensing.OSSLicensingService{}, - AuthTokenService: auth.NewFakeUserAuthTokenService(), + AuthTokenService: authtest.NewFakeUserAuthTokenService(), } hs.Cfg.CookieSecure = true @@ -564,7 +564,7 @@ func setupAuthProxyLoginTest(t *testing.T, enableLoginToken bool) *scenarioConte Cfg: sc.cfg, SettingsProvider: &setting.OSSImpl{Cfg: sc.cfg}, License: &licensing.OSSLicensingService{}, - AuthTokenService: auth.NewFakeUserAuthTokenService(), + AuthTokenService: authtest.NewFakeUserAuthTokenService(), log: log.New("hello"), SocialService: &mockSocialService{}, } @@ -602,7 +602,7 @@ func TestLoginPostRunLokingHook(t *testing.T) { log: log.New("test"), Cfg: setting.NewCfg(), License: &licensing.OSSLicensingService{}, - AuthTokenService: auth.NewFakeUserAuthTokenService(), + AuthTokenService: authtest.NewFakeUserAuthTokenService(), HooksService: hookService, } diff --git a/pkg/api/user_token.go b/pkg/api/user_token.go index 772cb0ff183..3e12fca2d2e 100644 --- a/pkg/api/user_token.go +++ b/pkg/api/user_token.go @@ -9,6 +9,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/web" @@ -43,7 +44,7 @@ func (hs *HTTPServer) GetUserAuthTokens(c *models.ReqContext) response.Response // 403: forbiddenError // 500: internalServerError func (hs *HTTPServer) RevokeUserAuthToken(c *models.ReqContext) response.Response { - cmd := models.RevokeAuthTokenCmd{} + cmd := auth.RevokeAuthTokenCmd{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) } @@ -143,7 +144,7 @@ func (hs *HTTPServer) getUserAuthTokensInternal(c *models.ReqContext, userID int return response.JSON(http.StatusOK, result) } -func (hs *HTTPServer) revokeUserAuthTokenInternal(c *models.ReqContext, userID int64, cmd models.RevokeAuthTokenCmd) response.Response { +func (hs *HTTPServer) revokeUserAuthTokenInternal(c *models.ReqContext, userID int64, cmd auth.RevokeAuthTokenCmd) response.Response { userQuery := user.GetUserByIDQuery{ID: userID} _, err := hs.userService.GetByID(c.Req.Context(), &userQuery) if err != nil { @@ -155,7 +156,7 @@ func (hs *HTTPServer) revokeUserAuthTokenInternal(c *models.ReqContext, userID i token, err := hs.AuthTokenService.GetUserToken(c.Req.Context(), userID, cmd.AuthTokenId) if err != nil { - if errors.Is(err, models.ErrUserTokenNotFound) { + if errors.Is(err, auth.ErrUserTokenNotFound) { return response.Error(404, "User auth token not found", err) } return response.Error(500, "Failed to get user auth token", err) @@ -167,7 +168,7 @@ func (hs *HTTPServer) revokeUserAuthTokenInternal(c *models.ReqContext, userID i err = hs.AuthTokenService.RevokeToken(c.Req.Context(), token, false) if err != nil { - if errors.Is(err, models.ErrUserTokenNotFound) { + if errors.Is(err, auth.ErrUserTokenNotFound) { return response.Error(404, "User auth token not found", err) } return response.Error(500, "Failed to revoke user auth token", err) @@ -182,11 +183,11 @@ func (hs *HTTPServer) revokeUserAuthTokenInternal(c *models.ReqContext, userID i type RevokeUserAuthTokenParams struct { // in:body // required:true - Body models.RevokeAuthTokenCmd `json:"body"` + Body auth.RevokeAuthTokenCmd `json:"body"` } // swagger:response getUserAuthTokensResponse type GetUserAuthTokensResponse struct { // in:body - Body []*models.UserToken `json:"body"` + Body []*auth.UserToken `json:"body"` } diff --git a/pkg/api/user_token_test.go b/pkg/api/user_token_test.go index 2dd7d7e7fbd..093a27011b9 100644 --- a/pkg/api/user_token_test.go +++ b/pkg/api/user_token_test.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/auth" + "github.com/grafana/grafana/pkg/services/auth/authtest" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/usertest" @@ -20,7 +21,7 @@ import ( func TestUserTokenAPIEndpoint(t *testing.T) { userMock := usertest.NewUserServiceFake() t.Run("When current user attempts to revoke an auth token for a non-existing user", func(t *testing.T) { - cmd := models.RevokeAuthTokenCmd{AuthTokenId: 2} + cmd := auth.RevokeAuthTokenCmd{AuthTokenId: 2} userMock.ExpectedError = user.ErrUserNotFound revokeUserAuthTokenScenario(t, "Should return not found when calling POST on", "/api/user/revoke-auth-token", "/api/user/revoke-auth-token", cmd, 200, func(sc *scenarioContext) { @@ -59,15 +60,15 @@ func TestUserTokenAPIEndpoint(t *testing.T) { }) t.Run("When revoke an auth token for a user", func(t *testing.T) { - cmd := models.RevokeAuthTokenCmd{AuthTokenId: 2} - token := &models.UserToken{Id: 1} + cmd := auth.RevokeAuthTokenCmd{AuthTokenId: 2} + token := &auth.UserToken{Id: 1} mockUser := &usertest.FakeUserService{ ExpectedUser: &user.User{ID: 200}, } revokeUserAuthTokenInternalScenario(t, "Should be successful", cmd, 200, token, func(sc *scenarioContext) { - sc.userAuthTokenService.GetUserTokenProvider = func(ctx context.Context, userId, userTokenId int64) (*models.UserToken, error) { - return &models.UserToken{Id: 2}, nil + sc.userAuthTokenService.GetUserTokenProvider = func(ctx context.Context, userId, userTokenId int64) (*auth.UserToken, error) { + return &auth.UserToken{Id: 2}, nil } sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() assert.Equal(t, 200, sc.resp.Code) @@ -75,11 +76,11 @@ func TestUserTokenAPIEndpoint(t *testing.T) { }) t.Run("When revoke the active auth token used by himself", func(t *testing.T) { - cmd := models.RevokeAuthTokenCmd{AuthTokenId: 2} - token := &models.UserToken{Id: 2} + cmd := auth.RevokeAuthTokenCmd{AuthTokenId: 2} + token := &auth.UserToken{Id: 2} mockUser := usertest.NewUserServiceFake() revokeUserAuthTokenInternalScenario(t, "Should not be successful", cmd, testUserID, token, func(sc *scenarioContext) { - sc.userAuthTokenService.GetUserTokenProvider = func(ctx context.Context, userId, userTokenId int64) (*models.UserToken, error) { + sc.userAuthTokenService.GetUserTokenProvider = func(ctx context.Context, userId, userTokenId int64) (*auth.UserToken, error) { return token, nil } sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() @@ -88,10 +89,10 @@ func TestUserTokenAPIEndpoint(t *testing.T) { }) t.Run("When gets auth tokens for a user", func(t *testing.T) { - currentToken := &models.UserToken{Id: 1} + currentToken := &auth.UserToken{Id: 1} mockUser := usertest.NewUserServiceFake() getUserAuthTokensInternalScenario(t, "Should be successful", currentToken, func(sc *scenarioContext) { - tokens := []*models.UserToken{ + tokens := []*auth.UserToken{ { Id: 1, ClientIp: "127.0.0.1", @@ -107,7 +108,7 @@ func TestUserTokenAPIEndpoint(t *testing.T) { SeenAt: 0, }, } - sc.userAuthTokenService.GetUserTokensProvider = func(ctx context.Context, userId int64) ([]*models.UserToken, error) { + sc.userAuthTokenService.GetUserTokensProvider = func(ctx context.Context, userId int64) ([]*auth.UserToken, error) { return tokens, nil } sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() @@ -145,10 +146,10 @@ func TestUserTokenAPIEndpoint(t *testing.T) { }) } -func revokeUserAuthTokenScenario(t *testing.T, desc string, url string, routePattern string, cmd models.RevokeAuthTokenCmd, +func revokeUserAuthTokenScenario(t *testing.T, desc string, url string, routePattern string, cmd auth.RevokeAuthTokenCmd, userId int64, fn scenarioFunc, userService user.Service) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { - fakeAuthTokenService := auth.NewFakeUserAuthTokenService() + fakeAuthTokenService := authtest.NewFakeUserAuthTokenService() hs := HTTPServer{ AuthTokenService: fakeAuthTokenService, @@ -175,7 +176,7 @@ func revokeUserAuthTokenScenario(t *testing.T, desc string, url string, routePat func getUserAuthTokensScenario(t *testing.T, desc string, url string, routePattern string, userId int64, fn scenarioFunc, userService user.Service) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { - fakeAuthTokenService := auth.NewFakeUserAuthTokenService() + fakeAuthTokenService := authtest.NewFakeUserAuthTokenService() hs := HTTPServer{ AuthTokenService: fakeAuthTokenService, @@ -202,7 +203,7 @@ func getUserAuthTokensScenario(t *testing.T, desc string, url string, routePatte func logoutUserFromAllDevicesInternalScenario(t *testing.T, desc string, userId int64, fn scenarioFunc, userService user.Service) { t.Run(desc, func(t *testing.T) { hs := HTTPServer{ - AuthTokenService: auth.NewFakeUserAuthTokenService(), + AuthTokenService: authtest.NewFakeUserAuthTokenService(), userService: userService, } @@ -222,10 +223,10 @@ func logoutUserFromAllDevicesInternalScenario(t *testing.T, desc string, userId }) } -func revokeUserAuthTokenInternalScenario(t *testing.T, desc string, cmd models.RevokeAuthTokenCmd, userId int64, - token *models.UserToken, fn scenarioFunc, userService user.Service) { +func revokeUserAuthTokenInternalScenario(t *testing.T, desc string, cmd auth.RevokeAuthTokenCmd, userId int64, + token *auth.UserToken, fn scenarioFunc, userService user.Service) { t.Run(desc, func(t *testing.T) { - fakeAuthTokenService := auth.NewFakeUserAuthTokenService() + fakeAuthTokenService := authtest.NewFakeUserAuthTokenService() hs := HTTPServer{ AuthTokenService: fakeAuthTokenService, @@ -248,9 +249,9 @@ func revokeUserAuthTokenInternalScenario(t *testing.T, desc string, cmd models.R }) } -func getUserAuthTokensInternalScenario(t *testing.T, desc string, token *models.UserToken, fn scenarioFunc, userService user.Service) { +func getUserAuthTokensInternalScenario(t *testing.T, desc string, token *auth.UserToken, fn scenarioFunc, userService user.Service) { t.Run(desc, func(t *testing.T) { - fakeAuthTokenService := auth.NewFakeUserAuthTokenService() + fakeAuthTokenService := authtest.NewFakeUserAuthTokenService() hs := HTTPServer{ AuthTokenService: fakeAuthTokenService, diff --git a/pkg/cmd/grafana-cli/runner/wire.go b/pkg/cmd/grafana-cli/runner/wire.go index 82efb1ce69e..8ee0af3f6a5 100644 --- a/pkg/cmd/grafana-cli/runner/wire.go +++ b/pkg/cmd/grafana-cli/runner/wire.go @@ -7,6 +7,7 @@ import ( "context" "github.com/google/wire" + "github.com/grafana/grafana/pkg/services/auth/authimpl" "github.com/grafana/grafana/pkg/tsdb/parca" "github.com/grafana/grafana/pkg/tsdb/phlare" @@ -253,8 +254,8 @@ var wireSet = wire.NewSet( influxdb.ProvideService, wire.Bind(new(social.Service), new(*social.SocialService)), oauthtoken.ProvideService, - auth.ProvideActiveAuthTokenService, - wire.Bind(new(auth.ActiveTokenService), new(*auth.ActiveAuthTokenService)), + authimpl.ProvideActiveAuthTokenService, + wire.Bind(new(auth.ActiveTokenService), new(*authimpl.ActiveAuthTokenService)), wire.Bind(new(oauthtoken.OAuthTokenService), new(*oauthtoken.Service)), tempo.ProvideService, loki.ProvideService, diff --git a/pkg/cmd/grafana-cli/runner/wireexts_oss.go b/pkg/cmd/grafana-cli/runner/wireexts_oss.go index 407ce3c1945..dfc36b3cd26 100644 --- a/pkg/cmd/grafana-cli/runner/wireexts_oss.go +++ b/pkg/cmd/grafana-cli/runner/wireexts_oss.go @@ -17,6 +17,7 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol" "github.com/grafana/grafana/pkg/services/auth" + "github.com/grafana/grafana/pkg/services/auth/authimpl" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/datasources/permissions" datasourceservice "github.com/grafana/grafana/pkg/services/datasources/service" @@ -48,9 +49,9 @@ var wireExtsSet = wire.NewSet( wire.Bind(new(setting.Provider), new(*setting.OSSImpl)), osskmsproviders.ProvideService, wire.Bind(new(kmsproviders.Service), new(osskmsproviders.Service)), - auth.ProvideUserAuthTokenService, - wire.Bind(new(models.UserTokenService), new(*auth.UserAuthTokenService)), - wire.Bind(new(models.UserTokenBackgroundService), new(*auth.UserAuthTokenService)), + authimpl.ProvideUserAuthTokenService, + wire.Bind(new(auth.UserTokenService), new(*authimpl.UserAuthTokenService)), + wire.Bind(new(auth.UserTokenBackgroundService), new(*authimpl.UserAuthTokenService)), acimpl.ProvideService, wire.Bind(new(accesscontrol.Service), new(*acimpl.Service)), wire.Bind(new(accesscontrol.RoleRegistry), new(*acimpl.Service)), diff --git a/pkg/middleware/auth.go b/pkg/middleware/auth.go index 7ab6274bc4c..b7e00465586 100644 --- a/pkg/middleware/auth.go +++ b/pkg/middleware/auth.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/middleware/cookies" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/team" @@ -42,7 +43,7 @@ func notAuthorized(c *models.ReqContext) { c.Redirect(setting.AppSubUrl + "/login") } -func tokenRevoked(c *models.ReqContext, err *models.TokenRevokedError) { +func tokenRevoked(c *models.ReqContext, err *auth.TokenRevokedError) { if c.IsApiRequest() { c.JSON(401, map[string]interface{}{ "message": "Token revoked", @@ -117,7 +118,7 @@ func Auth(options *AuthOptions) web.Handler { requireLogin := !c.AllowAnonymous || forceLogin || options.ReqNoAnonynmous if !c.IsSignedIn && options.ReqSignedIn && requireLogin { - var revokedErr *models.TokenRevokedError + var revokedErr *auth.TokenRevokedError if errors.As(c.LookupTokenErr, &revokedErr) { tokenRevoked(c, revokedErr) return diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index fec44973196..94efe1c7ee4 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -28,6 +28,7 @@ import ( "github.com/grafana/grafana/pkg/services/apikey" "github.com/grafana/grafana/pkg/services/apikey/apikeytest" "github.com/grafana/grafana/pkg/services/auth" + "github.com/grafana/grafana/pkg/services/auth/authtest" "github.com/grafana/grafana/pkg/services/contexthandler" "github.com/grafana/grafana/pkg/services/contexthandler/authproxy" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -264,8 +265,8 @@ func TestMiddlewareContext(t *testing.T) { sc.withTokenSessionCookie("token") sc.userService.ExpectedSignedInUser = &user.SignedInUser{OrgID: 2, UserID: userID} - sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) { - return &models.UserToken{ + sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*auth.UserToken, error) { + return &auth.UserToken{ UserId: userID, UnhashedToken: unhashedToken, }, nil @@ -288,14 +289,14 @@ func TestMiddlewareContext(t *testing.T) { sc.withTokenSessionCookie("token") sc.userService.ExpectedSignedInUser = &user.SignedInUser{OrgID: 2, UserID: userID} - sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) { - return &models.UserToken{ + sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*auth.UserToken, error) { + return &auth.UserToken{ UserId: userID, UnhashedToken: "", }, nil } - sc.userAuthTokenService.TryRotateTokenProvider = func(ctx context.Context, userToken *models.UserToken, + sc.userAuthTokenService.TryRotateTokenProvider = func(ctx context.Context, userToken *auth.UserToken, clientIP net.IP, userAgent string) (bool, error) { userToken.UnhashedToken = "rotated" return true, nil @@ -371,8 +372,8 @@ func TestMiddlewareContext(t *testing.T) { middlewareScenario(t, "Invalid/expired auth token in cookie", func(t *testing.T, sc *scenarioContext) { sc.withTokenSessionCookie("token") - sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) { - return nil, models.ErrUserTokenNotFound + sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*auth.UserToken, error) { + return nil, auth.ErrUserTokenNotFound } sc.fakeReq("GET", "/").exec() @@ -391,8 +392,8 @@ func TestMiddlewareContext(t *testing.T) { sc.userService.ExpectedSignedInUser = &user.SignedInUser{OrgID: 2, UserID: userID} sc.oauthTokenService.ExpectedAuthUser = &models.UserAuth{UserId: userID, OAuthExpiry: fakeGetTime()().Add(11 * time.Second)} - sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) { - return &models.UserToken{ + sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*auth.UserToken, error) { + return &auth.UserToken{ UserId: userID, UnhashedToken: unhashedToken, }, nil @@ -424,8 +425,8 @@ func TestMiddlewareContext(t *testing.T) { OAuthRefreshToken: "refresh_token"} sc.oauthTokenService.ExpectedErrors = map[string]error{"TryTokenRefresh": errors.New("error")} - sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) { - return &models.UserToken{ + sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*auth.UserToken, error) { + return &auth.UserToken{ UserId: userID, UnhashedToken: unhashedToken, }, nil @@ -454,8 +455,8 @@ func TestMiddlewareContext(t *testing.T) { sc.userService.ExpectedSignedInUser = &user.SignedInUser{OrgID: 2, UserID: userID} sc.oauthTokenService.ExpectedAuthUser = &models.UserAuth{UserId: userID, OAuthExpiry: fakeGetTime()().Add(-5 * time.Second), OAuthRefreshToken: "refreshtoken"} - sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) { - return &models.UserToken{ + sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*auth.UserToken, error) { + return &auth.UserToken{ UserId: userID, UnhashedToken: unhashedToken, }, nil @@ -481,8 +482,8 @@ func TestMiddlewareContext(t *testing.T) { sc.userService.ExpectedSignedInUser = &user.SignedInUser{OrgID: 2, UserID: userID} sc.oauthTokenService.ExpectedAuthUser = &models.UserAuth{UserId: userID} - sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) { - return &models.UserToken{ + sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*auth.UserToken, error) { + return &auth.UserToken{ UserId: userID, UnhashedToken: unhashedToken, }, nil @@ -819,14 +820,14 @@ func middlewareScenario(t *testing.T, desc string, fn scenarioFunc, cbs ...func( sc.userService = usertest.NewUserServiceFake() sc.orgService = orgtest.NewOrgServiceFake() sc.apiKeyService = &apikeytest.Service{} - sc.oauthTokenService = &auth.FakeOAuthTokenService{} + sc.oauthTokenService = &authtest.FakeOAuthTokenService{} ctxHdlr := getContextHandler(t, cfg, sc.mockSQLStore, sc.loginService, sc.apiKeyService, sc.userService, sc.orgService, sc.oauthTokenService) sc.sqlStore = ctxHdlr.SQLStore sc.contextHandler = ctxHdlr sc.m.Use(ctxHdlr.Middleware) sc.m.Use(OrgRedirect(sc.cfg, sc.userService)) - sc.userAuthTokenService = ctxHdlr.AuthTokenService.(*auth.FakeUserAuthTokenService) + sc.userAuthTokenService = ctxHdlr.AuthTokenService.(*authtest.FakeUserAuthTokenService) sc.jwtAuthService = ctxHdlr.JWTAuthService.(*models.FakeJWTService) sc.remoteCacheService = ctxHdlr.RemoteCache @@ -856,7 +857,7 @@ func middlewareScenario(t *testing.T, desc string, fn scenarioFunc, cbs ...func( func getContextHandler(t *testing.T, cfg *setting.Cfg, mockSQLStore *dbtest.FakeDB, loginService *loginservice.LoginServiceMock, apiKeyService *apikeytest.Service, userService *usertest.FakeUserService, orgService *orgtest.FakeOrgService, - oauthTokenService *auth.FakeOAuthTokenService, + oauthTokenService *authtest.FakeOAuthTokenService, ) *contexthandler.ContextHandler { t.Helper() @@ -868,7 +869,7 @@ func getContextHandler(t *testing.T, cfg *setting.Cfg, mockSQLStore *dbtest.Fake } remoteCacheSvc := remotecache.NewFakeStore(t) - userAuthTokenSvc := auth.NewFakeUserAuthTokenService() + userAuthTokenSvc := authtest.NewFakeUserAuthTokenService() renderSvc := &fakeRenderService{} authJWTSvc := models.NewFakeJWTService() tracer := tracing.InitializeTracerForTest() diff --git a/pkg/middleware/org_redirect_test.go b/pkg/middleware/org_redirect_test.go index 8d1460983df..810f04e56a1 100644 --- a/pkg/middleware/org_redirect_test.go +++ b/pkg/middleware/org_redirect_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/user" ) @@ -48,8 +48,8 @@ func TestOrgRedirectMiddleware(t *testing.T) { middlewareScenario(t, tc.desc, func(t *testing.T, sc *scenarioContext) { sc.withTokenSessionCookie("token") sc.userService.ExpectedSignedInUser = &user.SignedInUser{OrgID: 1, UserID: 12} - sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) { - return &models.UserToken{ + sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*auth.UserToken, error) { + return &auth.UserToken{ UserId: 0, UnhashedToken: "", }, nil @@ -68,8 +68,8 @@ func TestOrgRedirectMiddleware(t *testing.T) { sc.userService.ExpectedSetUsingOrgError = fmt.Errorf("") sc.userService.ExpectedSignedInUser = &user.SignedInUser{OrgID: 1, UserID: 12} - sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) { - return &models.UserToken{ + sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*auth.UserToken, error) { + return &auth.UserToken{ UserId: 12, UnhashedToken: "", }, nil diff --git a/pkg/middleware/quota_test.go b/pkg/middleware/quota_test.go index 446b7842933..156a626b10e 100644 --- a/pkg/middleware/quota_test.go +++ b/pkg/middleware/quota_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/assert" - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" @@ -55,8 +55,8 @@ func TestMiddlewareQuota(t *testing.T) { setUp := func(sc *scenarioContext) { sc.withTokenSessionCookie("token") sc.userService.ExpectedSignedInUser = &user.SignedInUser{UserID: 12} - sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) { - return &models.UserToken{ + sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*auth.UserToken, error) { + return &auth.UserToken{ UserId: 12, UnhashedToken: "", }, nil diff --git a/pkg/middleware/recovery_test.go b/pkg/middleware/recovery_test.go index 866eeaa490f..1a8fe8537c5 100644 --- a/pkg/middleware/recovery_test.go +++ b/pkg/middleware/recovery_test.go @@ -10,7 +10,7 @@ import ( "github.com/grafana/grafana/pkg/infra/remotecache" "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/auth" + "github.com/grafana/grafana/pkg/services/auth/authtest" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" ) @@ -65,7 +65,7 @@ func recoveryScenario(t *testing.T, desc string, url string, fn scenarioFunc) { sc.m.Use(AddDefaultResponseHeaders(cfg)) sc.m.UseMiddleware(web.Renderer(viewsPath, "[[", "]]")) - sc.userAuthTokenService = auth.NewFakeUserAuthTokenService() + sc.userAuthTokenService = authtest.NewFakeUserAuthTokenService() sc.remoteCacheService = remotecache.NewFakeStore(t) contextHandler := getContextHandler(t, nil, nil, nil, nil, nil, nil, nil) diff --git a/pkg/middleware/testing.go b/pkg/middleware/testing.go index a091f9118fe..7e858cfd845 100644 --- a/pkg/middleware/testing.go +++ b/pkg/middleware/testing.go @@ -13,7 +13,7 @@ import ( "github.com/grafana/grafana/pkg/infra/remotecache" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/apikey/apikeytest" - "github.com/grafana/grafana/pkg/services/auth" + "github.com/grafana/grafana/pkg/services/auth/authtest" "github.com/grafana/grafana/pkg/services/contexthandler" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" "github.com/grafana/grafana/pkg/services/login/loginservice" @@ -36,7 +36,7 @@ type scenarioContext struct { handlerFunc handlerFunc defaultHandler web.Handler url string - userAuthTokenService *auth.FakeUserAuthTokenService + userAuthTokenService *authtest.FakeUserAuthTokenService jwtAuthService *models.FakeJWTService remoteCacheService *remotecache.RemoteCache cfg *setting.Cfg @@ -46,7 +46,7 @@ type scenarioContext struct { loginService *loginservice.LoginServiceMock apiKeyService *apikeytest.Service userService *usertest.FakeUserService - oauthTokenService *auth.FakeOAuthTokenService + oauthTokenService *authtest.FakeOAuthTokenService orgService *orgtest.FakeOrgService req *http.Request diff --git a/pkg/models/context.go b/pkg/models/context.go index dd07b7236b8..432dee40e99 100644 --- a/pkg/models/context.go +++ b/pkg/models/context.go @@ -5,6 +5,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/models/usertoken" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" @@ -15,7 +16,7 @@ import ( type ReqContext struct { *web.Context *user.SignedInUser - UserToken *UserToken + UserToken *usertoken.UserToken IsSignedIn bool IsRenderCall bool diff --git a/pkg/models/usertoken/user_token.go b/pkg/models/usertoken/user_token.go new file mode 100644 index 00000000000..beb2ac1355f --- /dev/null +++ b/pkg/models/usertoken/user_token.go @@ -0,0 +1,26 @@ +package usertoken + +type TokenRevokedError struct { + UserID int64 + TokenID int64 + MaxConcurrentSessions int64 +} + +func (e *TokenRevokedError) Error() string { return "user token revoked" } + +// UserToken represents a user token +type UserToken struct { + Id int64 + UserId int64 + AuthToken string + PrevAuthToken string + UserAgent string + ClientIp string + AuthTokenSeen bool + SeenAt int64 + RotatedAt int64 + CreatedAt int64 + UpdatedAt int64 + RevokedAt int64 + UnhashedToken string +} diff --git a/pkg/server/backgroundsvcs/background_services.go b/pkg/server/backgroundsvcs/background_services.go index eaaa092aa08..2582d680327 100644 --- a/pkg/server/backgroundsvcs/background_services.go +++ b/pkg/server/backgroundsvcs/background_services.go @@ -7,10 +7,10 @@ import ( "github.com/grafana/grafana/pkg/infra/tracing" uss "github.com/grafana/grafana/pkg/infra/usagestats/service" "github.com/grafana/grafana/pkg/infra/usagestats/statscollector" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins/manager/process" "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/cleanup" "github.com/grafana/grafana/pkg/services/dashboardsnapshots" "github.com/grafana/grafana/pkg/services/grpcserver" @@ -38,7 +38,7 @@ import ( func ProvideBackgroundServiceRegistry( httpServer *api.HTTPServer, ng *ngalert.AlertNG, cleanup *cleanup.CleanUpService, live *live.GrafanaLive, pushGateway *pushhttp.Gateway, notifications *notifications.NotificationService, processManager *process.Manager, - rendering *rendering.RenderingService, tokenService models.UserTokenBackgroundService, tracing tracing.Tracer, + rendering *rendering.RenderingService, tokenService auth.UserTokenBackgroundService, tracing tracing.Tracer, provisioning *provisioning.ProvisioningServiceImpl, alerting *alerting.AlertEngine, usageStats *uss.UsageStats, statsCollector *statscollector.Service, grafanaUpdateChecker *updatechecker.GrafanaService, pluginsUpdateChecker *updatechecker.PluginsService, metrics *metrics.InternalMetricsService, diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 57338f5d46c..4d2802abbe8 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -49,6 +49,7 @@ import ( "github.com/grafana/grafana/pkg/services/annotations/annotationsimpl" "github.com/grafana/grafana/pkg/services/apikey/apikeyimpl" "github.com/grafana/grafana/pkg/services/auth" + "github.com/grafana/grafana/pkg/services/auth/authimpl" "github.com/grafana/grafana/pkg/services/auth/jwt" "github.com/grafana/grafana/pkg/services/cleanup" "github.com/grafana/grafana/pkg/services/comments" @@ -271,8 +272,8 @@ var wireBasicSet = wire.NewSet( influxdb.ProvideService, wire.Bind(new(social.Service), new(*social.SocialService)), oauthtoken.ProvideService, - auth.ProvideActiveAuthTokenService, - wire.Bind(new(auth.ActiveTokenService), new(*auth.ActiveAuthTokenService)), + authimpl.ProvideActiveAuthTokenService, + wire.Bind(new(auth.ActiveTokenService), new(*authimpl.ActiveAuthTokenService)), wire.Bind(new(oauthtoken.OAuthTokenService), new(*oauthtoken.Service)), tempo.ProvideService, loki.ProvideService, diff --git a/pkg/server/wireexts_oss.go b/pkg/server/wireexts_oss.go index 5c366331875..6fae56eaaca 100644 --- a/pkg/server/wireexts_oss.go +++ b/pkg/server/wireexts_oss.go @@ -17,6 +17,7 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol" "github.com/grafana/grafana/pkg/services/auth" + "github.com/grafana/grafana/pkg/services/auth/authimpl" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/datasources/permissions" datasourceservice "github.com/grafana/grafana/pkg/services/datasources/service" @@ -39,9 +40,9 @@ import ( ) var wireExtsBasicSet = wire.NewSet( - auth.ProvideUserAuthTokenService, - wire.Bind(new(models.UserTokenService), new(*auth.UserAuthTokenService)), - wire.Bind(new(models.UserTokenBackgroundService), new(*auth.UserAuthTokenService)), + authimpl.ProvideUserAuthTokenService, + wire.Bind(new(auth.UserTokenService), new(*authimpl.UserAuthTokenService)), + wire.Bind(new(auth.UserTokenBackgroundService), new(*authimpl.UserAuthTokenService)), licensing.ProvideService, wire.Bind(new(models.Licensing), new(*licensing.OSSLicensingService)), setting.ProvideProvider, diff --git a/pkg/services/accesscontrol/middleware.go b/pkg/services/accesscontrol/middleware.go index c0d3868e266..99b93d329dd 100644 --- a/pkg/services/accesscontrol/middleware.go +++ b/pkg/services/accesscontrol/middleware.go @@ -14,8 +14,8 @@ import ( "time" "github.com/grafana/grafana/pkg/middleware/cookies" - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/models/usertoken" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" @@ -41,7 +41,7 @@ func Middleware(ac AccessControl) func(web.Handler, Evaluator) web.Handler { } } - var revokedErr *models.TokenRevokedError + var revokedErr *usertoken.TokenRevokedError if errors.As(c.LookupTokenErr, &revokedErr) { unauthorized(c, revokedErr) return @@ -111,7 +111,7 @@ func unauthorized(c *models.ReqContext, err error) { "message": "Unauthorized", } - var revokedErr *models.TokenRevokedError + var revokedErr *usertoken.TokenRevokedError if errors.As(err, &revokedErr) { response["message"] = "Token revoked" response["error"] = map[string]interface{}{ diff --git a/pkg/models/user_token.go b/pkg/services/auth/auth.go similarity index 74% rename from pkg/models/user_token.go rename to pkg/services/auth/auth.go index 6c92a40d86b..77b21316707 100644 --- a/pkg/models/user_token.go +++ b/pkg/services/auth/auth.go @@ -1,19 +1,32 @@ -package models +package auth import ( "context" "errors" "net" + "github.com/grafana/grafana/pkg/models/usertoken" "github.com/grafana/grafana/pkg/registry" + "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/user" ) +const ( + QuotaTargetSrv quota.TargetSrv = "auth" + QuotaTarget quota.Target = "session" +) + +type ActiveTokenService interface { + ActiveTokenCount(ctx context.Context, _ *quota.ScopeParameters) (*quota.Map, error) +} + // Typed errors var ( ErrUserTokenNotFound = errors.New("user token not found") ) +type TokenRevokedError = usertoken.TokenRevokedError + // CreateTokenErr represents a token creation error; used in Enterprise type CreateTokenErr struct { StatusCode int @@ -35,30 +48,7 @@ type TokenExpiredError struct { func (e *TokenExpiredError) Error() string { return "user token expired" } -type TokenRevokedError struct { - UserID int64 - TokenID int64 - MaxConcurrentSessions int64 -} - -func (e *TokenRevokedError) Error() string { return "user token revoked" } - -// UserToken represents a user token -type UserToken struct { - Id int64 - UserId int64 - AuthToken string - PrevAuthToken string - UserAgent string - ClientIp string - AuthTokenSeen bool - SeenAt int64 - RotatedAt int64 - CreatedAt int64 - UpdatedAt int64 - RevokedAt int64 - UnhashedToken string -} +type UserToken = usertoken.UserToken type RevokeAuthTokenCmd struct { AuthTokenId int64 `json:"authTokenId"` diff --git a/pkg/services/auth/auth_token.go b/pkg/services/auth/authimpl/auth_token.go similarity index 91% rename from pkg/services/auth/auth_token.go rename to pkg/services/auth/authimpl/auth_token.go index f261e33bcd1..c969686a2f8 100644 --- a/pkg/services/auth/auth_token.go +++ b/pkg/services/auth/authimpl/auth_token.go @@ -1,4 +1,4 @@ -package auth +package authimpl import ( "context" @@ -11,7 +11,7 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/serverlock" - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" @@ -42,10 +42,6 @@ type UserAuthTokenService struct { log log.Logger } -type ActiveTokenService interface { - ActiveTokenCount(ctx context.Context, _ *quota.ScopeParameters) (*quota.Map, error) -} - type ActiveAuthTokenService struct { cfg *setting.Cfg sqlStore db.DB @@ -63,7 +59,7 @@ func ProvideActiveAuthTokenService(cfg *setting.Cfg, sqlStore db.DB, quotaServic } if err := quotaService.RegisterQuotaReporter("a.NewUsageReporter{ - TargetSrv: QuotaTargetSrv, + TargetSrv: auth.QuotaTargetSrv, DefaultLimits: defaultLimits, Reporter: s.ActiveTokenCount, }); err != nil { @@ -86,7 +82,7 @@ func (a *ActiveAuthTokenService) ActiveTokenCount(ctx context.Context, _ *quota. return err }) - tag, err := quota.NewTag(QuotaTargetSrv, QuotaTarget, quota.GlobalScope) + tag, err := quota.NewTag(auth.QuotaTargetSrv, auth.QuotaTarget, quota.GlobalScope) if err != nil { return nil, err } @@ -96,7 +92,7 @@ func (a *ActiveAuthTokenService) ActiveTokenCount(ctx context.Context, _ *quota. return u, err } -func (s *UserAuthTokenService) CreateToken(ctx context.Context, user *user.User, clientIP net.IP, userAgent string) (*models.UserToken, error) { +func (s *UserAuthTokenService) CreateToken(ctx context.Context, user *user.User, clientIP net.IP, userAgent string) (*auth.UserToken, error) { token, err := util.RandomHex(16) if err != nil { return nil, err @@ -138,13 +134,13 @@ func (s *UserAuthTokenService) CreateToken(ctx context.Context, user *user.User, ctxLogger := s.log.FromContext(ctx) ctxLogger.Debug("user auth token created", "tokenId", userAuthToken.Id, "userId", userAuthToken.UserId, "clientIP", userAuthToken.ClientIp, "userAgent", userAuthToken.UserAgent, "authToken", userAuthToken.AuthToken) - var userToken models.UserToken + var userToken auth.UserToken err = userAuthToken.toUserToken(&userToken) return &userToken, err } -func (s *UserAuthTokenService) LookupToken(ctx context.Context, unhashedToken string) (*models.UserToken, error) { +func (s *UserAuthTokenService) LookupToken(ctx context.Context, unhashedToken string) (*auth.UserToken, error) { hashedToken := hashToken(unhashedToken) var model userAuthToken var exists bool @@ -162,14 +158,14 @@ func (s *UserAuthTokenService) LookupToken(ctx context.Context, unhashedToken st } if !exists { - return nil, models.ErrUserTokenNotFound + return nil, auth.ErrUserTokenNotFound } ctxLogger := s.log.FromContext(ctx) if model.RevokedAt > 0 { ctxLogger.Debug("user token has been revoked", "user ID", model.UserId, "token ID", model.Id) - return nil, &models.TokenRevokedError{ + return nil, &auth.TokenRevokedError{ UserID: model.UserId, TokenID: model.Id, } @@ -177,7 +173,7 @@ func (s *UserAuthTokenService) LookupToken(ctx context.Context, unhashedToken st if model.CreatedAt <= s.createdAfterParam() || model.RotatedAt <= s.rotatedAfterParam() { ctxLogger.Debug("user token has expired", "user ID", model.UserId, "token ID", model.Id) - return nil, &models.TokenExpiredError{ + return nil, &auth.TokenExpiredError{ UserID: model.UserId, TokenID: model.Id, } @@ -242,13 +238,13 @@ func (s *UserAuthTokenService) LookupToken(ctx context.Context, unhashedToken st model.UnhashedToken = unhashedToken - var userToken models.UserToken + var userToken auth.UserToken err = model.toUserToken(&userToken) return &userToken, err } -func (s *UserAuthTokenService) TryRotateToken(ctx context.Context, token *models.UserToken, +func (s *UserAuthTokenService) TryRotateToken(ctx context.Context, token *auth.UserToken, clientIP net.IP, userAgent string) (bool, error) { if token == nil { return false, nil @@ -328,9 +324,9 @@ func (s *UserAuthTokenService) TryRotateToken(ctx context.Context, token *models return false, nil } -func (s *UserAuthTokenService) RevokeToken(ctx context.Context, token *models.UserToken, soft bool) error { +func (s *UserAuthTokenService) RevokeToken(ctx context.Context, token *auth.UserToken, soft bool) error { if token == nil { - return models.ErrUserTokenNotFound + return auth.ErrUserTokenNotFound } model, err := userAuthTokenFromUserToken(token) @@ -361,7 +357,7 @@ func (s *UserAuthTokenService) RevokeToken(ctx context.Context, token *models.Us if rowsAffected == 0 { ctxLogger.Debug("user auth token not found/revoked", "tokenId", model.Id, "userId", model.UserId, "clientIP", model.ClientIp, "userAgent", model.UserAgent) - return models.ErrUserTokenNotFound + return auth.ErrUserTokenNotFound } ctxLogger.Debug("user auth token revoked", "tokenId", model.Id, "userId", model.UserId, "clientIP", model.ClientIp, "userAgent", model.UserAgent, "soft", soft) @@ -418,8 +414,8 @@ func (s *UserAuthTokenService) BatchRevokeAllUserTokens(ctx context.Context, use }) } -func (s *UserAuthTokenService) GetUserToken(ctx context.Context, userId, userTokenId int64) (*models.UserToken, error) { - var result models.UserToken +func (s *UserAuthTokenService) GetUserToken(ctx context.Context, userId, userTokenId int64) (*auth.UserToken, error) { + var result auth.UserToken err := s.SQLStore.WithDbSession(ctx, func(dbSession *db.Session) error { var token userAuthToken exists, err := dbSession.Where("id = ? AND user_id = ?", userTokenId, userId).Get(&token) @@ -428,7 +424,7 @@ func (s *UserAuthTokenService) GetUserToken(ctx context.Context, userId, userTok } if !exists { - return models.ErrUserTokenNotFound + return auth.ErrUserTokenNotFound } return token.toUserToken(&result) @@ -437,8 +433,8 @@ func (s *UserAuthTokenService) GetUserToken(ctx context.Context, userId, userTok return &result, err } -func (s *UserAuthTokenService) GetUserTokens(ctx context.Context, userId int64) ([]*models.UserToken, error) { - result := []*models.UserToken{} +func (s *UserAuthTokenService) GetUserTokens(ctx context.Context, userId int64) ([]*auth.UserToken, error) { + result := []*auth.UserToken{} err := s.SQLStore.WithDbSession(ctx, func(dbSession *db.Session) error { var tokens []*userAuthToken err := dbSession.Where("user_id = ? AND created_at > ? AND rotated_at > ? AND revoked_at = 0", @@ -451,7 +447,7 @@ func (s *UserAuthTokenService) GetUserTokens(ctx context.Context, userId int64) } for _, token := range tokens { - var userToken models.UserToken + var userToken auth.UserToken if err := token.toUserToken(&userToken); err != nil { return err } @@ -464,8 +460,8 @@ func (s *UserAuthTokenService) GetUserTokens(ctx context.Context, userId int64) return result, err } -func (s *UserAuthTokenService) GetUserRevokedTokens(ctx context.Context, userId int64) ([]*models.UserToken, error) { - result := []*models.UserToken{} +func (s *UserAuthTokenService) GetUserRevokedTokens(ctx context.Context, userId int64) ([]*auth.UserToken, error) { + result := []*auth.UserToken{} err := s.SQLStore.WithDbSession(ctx, func(dbSession *db.Session) error { var tokens []*userAuthToken err := dbSession.Where("user_id = ? AND revoked_at > 0", userId).Find(&tokens) @@ -474,7 +470,7 @@ func (s *UserAuthTokenService) GetUserRevokedTokens(ctx context.Context, userId } for _, token := range tokens { - var userToken models.UserToken + var userToken auth.UserToken if err := token.toUserToken(&userToken); err != nil { return err } @@ -507,7 +503,7 @@ func readQuotaConfig(cfg *setting.Cfg) (*quota.Map, error) { return limits, nil } - globalQuotaTag, err := quota.NewTag(QuotaTargetSrv, QuotaTarget, quota.GlobalScope) + globalQuotaTag, err := quota.NewTag(auth.QuotaTargetSrv, auth.QuotaTarget, quota.GlobalScope) if err != nil { return limits, err } diff --git a/pkg/services/auth/auth_token_test.go b/pkg/services/auth/authimpl/auth_token_test.go similarity index 96% rename from pkg/services/auth/auth_token_test.go rename to pkg/services/auth/authimpl/auth_token_test.go index 16886d7b439..97528bf6be2 100644 --- a/pkg/services/auth/auth_token_test.go +++ b/pkg/services/auth/authimpl/auth_token_test.go @@ -1,4 +1,4 @@ -package auth +package authimpl import ( "context" @@ -8,12 +8,12 @@ import ( "testing" "time" + "github.com/grafana/grafana/pkg/services/auth" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" @@ -29,7 +29,7 @@ func TestUserAuthToken(t *testing.T) { defer func() { getTime = time.Now }() t.Run("When creating token", func(t *testing.T) { - createToken := func() *models.UserToken { + createToken := func() *auth.UserToken { userToken, err := ctx.tokenService.CreateToken(context.Background(), user, net.ParseIP("192.168.10.11"), "some user agent") require.Nil(t, err) @@ -43,7 +43,7 @@ func TestUserAuthToken(t *testing.T) { t.Run("Can count active tokens", func(t *testing.T) { m, err := ctx.activeTokenService.ActiveTokenCount(context.Background(), "a.ScopeParameters{}) require.Nil(t, err) - tag, err := quota.NewTag(QuotaTargetSrv, QuotaTarget, quota.GlobalScope) + tag, err := quota.NewTag(auth.QuotaTargetSrv, auth.QuotaTarget, quota.GlobalScope) require.NoError(t, err) count, ok := m.Get(tag) require.True(t, ok) @@ -65,7 +65,7 @@ func TestUserAuthToken(t *testing.T) { t.Run("When lookup hashed token should return user auth token not found error", func(t *testing.T) { userToken, err := ctx.tokenService.LookupToken(context.Background(), userToken.AuthToken) - require.Equal(t, models.ErrUserTokenNotFound, err) + require.Equal(t, auth.ErrUserTokenNotFound, err) require.Nil(t, userToken) }) @@ -90,13 +90,13 @@ func TestUserAuthToken(t *testing.T) { t.Run("revoking nil token should return error", func(t *testing.T) { err := ctx.tokenService.RevokeToken(context.Background(), nil, false) - require.Equal(t, models.ErrUserTokenNotFound, err) + require.Equal(t, auth.ErrUserTokenNotFound, err) }) t.Run("revoking non-existing token should return error", func(t *testing.T) { userToken.Id = 1000 err := ctx.tokenService.RevokeToken(context.Background(), userToken, false) - require.Equal(t, models.ErrUserTokenNotFound, err) + require.Equal(t, auth.ErrUserTokenNotFound, err) }) ctx = createTestContext(t) @@ -209,13 +209,13 @@ func TestUserAuthToken(t *testing.T) { } notGood, err := ctx.tokenService.LookupToken(context.Background(), userToken.UnhashedToken) - require.Equal(t, reflect.TypeOf(err), reflect.TypeOf(&models.TokenExpiredError{})) + require.Equal(t, reflect.TypeOf(err), reflect.TypeOf(&auth.TokenExpiredError{})) require.Nil(t, notGood) t.Run("should not find active token when expired", func(t *testing.T) { m, err := ctx.activeTokenService.ActiveTokenCount(context.Background(), "a.ScopeParameters{}) require.Nil(t, err) - tag, err := quota.NewTag(QuotaTargetSrv, QuotaTarget, quota.GlobalScope) + tag, err := quota.NewTag(auth.QuotaTargetSrv, auth.QuotaTarget, quota.GlobalScope) require.NoError(t, err) count, ok := m.Get(tag) require.True(t, ok) @@ -247,7 +247,7 @@ func TestUserAuthToken(t *testing.T) { } notGood, err := ctx.tokenService.LookupToken(context.Background(), userToken.UnhashedToken) - require.Equal(t, reflect.TypeOf(err), reflect.TypeOf(&models.TokenExpiredError{})) + require.Equal(t, reflect.TypeOf(err), reflect.TypeOf(&auth.TokenExpiredError{})) require.Nil(t, notGood) }) }) @@ -274,7 +274,7 @@ func TestUserAuthToken(t *testing.T) { model, err := ctx.getAuthTokenByID(userToken.Id) require.Nil(t, err) - var tok models.UserToken + var tok auth.UserToken err = model.toUserToken(&tok) require.Nil(t, err) @@ -471,7 +471,7 @@ func TestUserAuthToken(t *testing.T) { }) t.Run("When populating userAuthToken from UserToken should copy all properties", func(t *testing.T) { - ut := models.UserToken{ + ut := auth.UserToken{ Id: 1, UserId: 2, AuthToken: "a", @@ -524,7 +524,7 @@ func TestUserAuthToken(t *testing.T) { require.Nil(t, err) uatMap := uatJSON.MustMap() - var ut models.UserToken + var ut auth.UserToken err = uat.toUserToken(&ut) require.Nil(t, err) utBytes, err := json.Marshal(ut) diff --git a/pkg/services/auth/model.go b/pkg/services/auth/authimpl/model.go similarity index 77% rename from pkg/services/auth/model.go rename to pkg/services/auth/authimpl/model.go index afc5b566c48..407927df572 100644 --- a/pkg/services/auth/model.go +++ b/pkg/services/auth/authimpl/model.go @@ -1,10 +1,9 @@ -package auth +package authimpl import ( "fmt" - "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/quota" + "github.com/grafana/grafana/pkg/services/auth" ) type userAuthToken struct { @@ -23,13 +22,13 @@ type userAuthToken struct { UnhashedToken string `xorm:"-"` } -func userAuthTokenFromUserToken(ut *models.UserToken) (*userAuthToken, error) { +func userAuthTokenFromUserToken(ut *auth.UserToken) (*userAuthToken, error) { var uat userAuthToken err := uat.fromUserToken(ut) return &uat, err } -func (uat *userAuthToken) fromUserToken(ut *models.UserToken) error { +func (uat *userAuthToken) fromUserToken(ut *auth.UserToken) error { if uat == nil { return fmt.Errorf("needs pointer to userAuthToken struct") } @@ -51,7 +50,7 @@ func (uat *userAuthToken) fromUserToken(ut *models.UserToken) error { return nil } -func (uat *userAuthToken) toUserToken(ut *models.UserToken) error { +func (uat *userAuthToken) toUserToken(ut *auth.UserToken) error { if uat == nil { return fmt.Errorf("needs pointer to userAuthToken struct") } @@ -72,8 +71,3 @@ func (uat *userAuthToken) toUserToken(ut *models.UserToken) error { return nil } - -const ( - QuotaTargetSrv quota.TargetSrv = "auth" - QuotaTarget quota.Target = "session" -) diff --git a/pkg/services/auth/token_cleanup.go b/pkg/services/auth/authimpl/token_cleanup.go similarity index 99% rename from pkg/services/auth/token_cleanup.go rename to pkg/services/auth/authimpl/token_cleanup.go index a82f13630fe..08d8ae7c614 100644 --- a/pkg/services/auth/token_cleanup.go +++ b/pkg/services/auth/authimpl/token_cleanup.go @@ -1,4 +1,4 @@ -package auth +package authimpl import ( "context" diff --git a/pkg/services/auth/token_cleanup_test.go b/pkg/services/auth/authimpl/token_cleanup_test.go similarity index 99% rename from pkg/services/auth/token_cleanup_test.go rename to pkg/services/auth/authimpl/token_cleanup_test.go index a39e0d7892b..e207448c8f6 100644 --- a/pkg/services/auth/token_cleanup_test.go +++ b/pkg/services/auth/authimpl/token_cleanup_test.go @@ -1,4 +1,4 @@ -package auth +package authimpl import ( "context" diff --git a/pkg/services/auth/testing.go b/pkg/services/auth/authtest/testing.go similarity index 77% rename from pkg/services/auth/testing.go rename to pkg/services/auth/authtest/testing.go index 63b08a9a639..d2bbd09b46b 100644 --- a/pkg/services/auth/testing.go +++ b/pkg/services/auth/authtest/testing.go @@ -1,4 +1,4 @@ -package auth +package authtest import ( "context" @@ -6,42 +6,43 @@ import ( "time" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/user" "golang.org/x/oauth2" ) type FakeUserAuthTokenService struct { - CreateTokenProvider func(ctx context.Context, user *user.User, clientIP net.IP, userAgent string) (*models.UserToken, error) - TryRotateTokenProvider func(ctx context.Context, token *models.UserToken, clientIP net.IP, userAgent string) (bool, error) - LookupTokenProvider func(ctx context.Context, unhashedToken string) (*models.UserToken, error) - RevokeTokenProvider func(ctx context.Context, token *models.UserToken, soft bool) error + CreateTokenProvider func(ctx context.Context, user *user.User, clientIP net.IP, userAgent string) (*auth.UserToken, error) + TryRotateTokenProvider func(ctx context.Context, token *auth.UserToken, clientIP net.IP, userAgent string) (bool, error) + LookupTokenProvider func(ctx context.Context, unhashedToken string) (*auth.UserToken, error) + RevokeTokenProvider func(ctx context.Context, token *auth.UserToken, soft bool) error RevokeAllUserTokensProvider func(ctx context.Context, userId int64) error ActiveAuthTokenCount func(ctx context.Context) (int64, error) - GetUserTokenProvider func(ctx context.Context, userId, userTokenId int64) (*models.UserToken, error) - GetUserTokensProvider func(ctx context.Context, userId int64) ([]*models.UserToken, error) - GetUserRevokedTokensProvider func(ctx context.Context, userId int64) ([]*models.UserToken, error) + GetUserTokenProvider func(ctx context.Context, userId, userTokenId int64) (*auth.UserToken, error) + GetUserTokensProvider func(ctx context.Context, userId int64) ([]*auth.UserToken, error) + GetUserRevokedTokensProvider func(ctx context.Context, userId int64) ([]*auth.UserToken, error) BatchRevokedTokenProvider func(ctx context.Context, userIds []int64) error } func NewFakeUserAuthTokenService() *FakeUserAuthTokenService { return &FakeUserAuthTokenService{ - CreateTokenProvider: func(ctx context.Context, user *user.User, clientIP net.IP, userAgent string) (*models.UserToken, error) { - return &models.UserToken{ + CreateTokenProvider: func(ctx context.Context, user *user.User, clientIP net.IP, userAgent string) (*auth.UserToken, error) { + return &auth.UserToken{ UserId: 0, UnhashedToken: "", }, nil }, - TryRotateTokenProvider: func(ctx context.Context, token *models.UserToken, clientIP net.IP, userAgent string) (bool, error) { + TryRotateTokenProvider: func(ctx context.Context, token *auth.UserToken, clientIP net.IP, userAgent string) (bool, error) { return false, nil }, - LookupTokenProvider: func(ctx context.Context, unhashedToken string) (*models.UserToken, error) { - return &models.UserToken{ + LookupTokenProvider: func(ctx context.Context, unhashedToken string) (*auth.UserToken, error) { + return &auth.UserToken{ UserId: 0, UnhashedToken: "", }, nil }, - RevokeTokenProvider: func(ctx context.Context, token *models.UserToken, soft bool) error { + RevokeTokenProvider: func(ctx context.Context, token *auth.UserToken, soft bool) error { return nil }, RevokeAllUserTokensProvider: func(ctx context.Context, userId int64) error { @@ -53,10 +54,10 @@ func NewFakeUserAuthTokenService() *FakeUserAuthTokenService { ActiveAuthTokenCount: func(ctx context.Context) (int64, error) { return 10, nil }, - GetUserTokenProvider: func(ctx context.Context, userId, userTokenId int64) (*models.UserToken, error) { + GetUserTokenProvider: func(ctx context.Context, userId, userTokenId int64) (*auth.UserToken, error) { return nil, nil }, - GetUserTokensProvider: func(ctx context.Context, userId int64) ([]*models.UserToken, error) { + GetUserTokensProvider: func(ctx context.Context, userId int64) ([]*auth.UserToken, error) { return nil, nil }, } @@ -68,20 +69,20 @@ func (s *FakeUserAuthTokenService) Init() error { return nil } -func (s *FakeUserAuthTokenService) CreateToken(ctx context.Context, user *user.User, clientIP net.IP, userAgent string) (*models.UserToken, error) { +func (s *FakeUserAuthTokenService) CreateToken(ctx context.Context, user *user.User, clientIP net.IP, userAgent string) (*auth.UserToken, error) { return s.CreateTokenProvider(context.Background(), user, clientIP, userAgent) } -func (s *FakeUserAuthTokenService) LookupToken(ctx context.Context, unhashedToken string) (*models.UserToken, error) { +func (s *FakeUserAuthTokenService) LookupToken(ctx context.Context, unhashedToken string) (*auth.UserToken, error) { return s.LookupTokenProvider(context.Background(), unhashedToken) } -func (s *FakeUserAuthTokenService) TryRotateToken(ctx context.Context, token *models.UserToken, clientIP net.IP, +func (s *FakeUserAuthTokenService) TryRotateToken(ctx context.Context, token *auth.UserToken, clientIP net.IP, userAgent string) (bool, error) { return s.TryRotateTokenProvider(context.Background(), token, clientIP, userAgent) } -func (s *FakeUserAuthTokenService) RevokeToken(ctx context.Context, token *models.UserToken, soft bool) error { +func (s *FakeUserAuthTokenService) RevokeToken(ctx context.Context, token *auth.UserToken, soft bool) error { return s.RevokeTokenProvider(context.Background(), token, soft) } @@ -93,15 +94,15 @@ func (s *FakeUserAuthTokenService) ActiveTokenCount(ctx context.Context) (int64, return s.ActiveAuthTokenCount(context.Background()) } -func (s *FakeUserAuthTokenService) GetUserToken(ctx context.Context, userId, userTokenId int64) (*models.UserToken, error) { +func (s *FakeUserAuthTokenService) GetUserToken(ctx context.Context, userId, userTokenId int64) (*auth.UserToken, error) { return s.GetUserTokenProvider(context.Background(), userId, userTokenId) } -func (s *FakeUserAuthTokenService) GetUserTokens(ctx context.Context, userId int64) ([]*models.UserToken, error) { +func (s *FakeUserAuthTokenService) GetUserTokens(ctx context.Context, userId int64) ([]*auth.UserToken, error) { return s.GetUserTokensProvider(context.Background(), userId) } -func (s *FakeUserAuthTokenService) GetUserRevokedTokens(ctx context.Context, userId int64) ([]*models.UserToken, error) { +func (s *FakeUserAuthTokenService) GetUserRevokedTokens(ctx context.Context, userId int64) ([]*auth.UserToken, error) { return s.GetUserRevokedTokensProvider(context.Background(), userId) } diff --git a/pkg/services/contexthandler/auth_proxy_test.go b/pkg/services/contexthandler/auth_proxy_test.go index 307ca2b51bc..dc697eff484 100644 --- a/pkg/services/contexthandler/auth_proxy_test.go +++ b/pkg/services/contexthandler/auth_proxy_test.go @@ -13,7 +13,7 @@ import ( "github.com/grafana/grafana/pkg/infra/remotecache" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/auth" + "github.com/grafana/grafana/pkg/services/auth/authtest" "github.com/grafana/grafana/pkg/services/contexthandler/authproxy" "github.com/grafana/grafana/pkg/services/login/loginservice" "github.com/grafana/grafana/pkg/services/org/orgtest" @@ -80,7 +80,7 @@ func getContextHandler(t *testing.T) *ContextHandler { cfg.AuthProxyHeaderProperty = "username" remoteCacheSvc, err := remotecache.ProvideService(cfg, sqlStore) require.NoError(t, err) - userAuthTokenSvc := auth.NewFakeUserAuthTokenService() + userAuthTokenSvc := authtest.NewFakeUserAuthTokenService() renderSvc := &fakeRenderService{} authJWTSvc := models.NewFakeJWTService() tracer := tracing.InitializeTracerForTest() diff --git a/pkg/services/contexthandler/contexthandler.go b/pkg/services/contexthandler/contexthandler.go index c8d67f165c0..6e2be2233c5 100644 --- a/pkg/services/contexthandler/contexthandler.go +++ b/pkg/services/contexthandler/contexthandler.go @@ -44,7 +44,7 @@ const ( const ServiceName = "ContextHandler" -func ProvideService(cfg *setting.Cfg, tokenService models.UserTokenService, jwtService models.JWTService, +func ProvideService(cfg *setting.Cfg, tokenService auth.UserTokenService, jwtService models.JWTService, remoteCache *remotecache.RemoteCache, renderService rendering.Service, sqlStore db.DB, tracer tracing.Tracer, authProxy *authproxy.AuthProxy, loginService login.Service, apiKeyService apikey.Service, authenticator loginpkg.Authenticator, userService user.Service, @@ -77,7 +77,7 @@ func ProvideService(cfg *setting.Cfg, tokenService models.UserTokenService, jwtS // ContextHandler is a middleware. type ContextHandler struct { Cfg *setting.Cfg - AuthTokenService models.UserTokenService + AuthTokenService auth.UserTokenService JWTAuthService models.JWTService RemoteCache *remotecache.RemoteCache RenderService rendering.Service @@ -474,7 +474,7 @@ func (h *ContextHandler) initContextWithToken(reqContext *models.ReqContext, org } err = h.AuthTokenService.RevokeToken(ctx, token, false) - if err != nil && !errors.Is(err, models.ErrUserTokenNotFound) { + if err != nil && !errors.Is(err, auth.ErrUserTokenNotFound) { reqContext.Logger.Error("failed to revoke auth token", "error", err) } return false @@ -506,8 +506,8 @@ func (h *ContextHandler) deleteInvalidCookieEndOfRequestFunc(reqContext *models. } } -func (h *ContextHandler) rotateEndOfRequestFunc(reqContext *models.ReqContext, authTokenService models.UserTokenService, - token *models.UserToken) web.BeforeFunc { +func (h *ContextHandler) rotateEndOfRequestFunc(reqContext *models.ReqContext, authTokenService auth.UserTokenService, + token *auth.UserToken) web.BeforeFunc { return func(w web.ResponseWriter) { // if response has already been written, skip. if w.Written() { diff --git a/pkg/services/contexthandler/contexthandler_test.go b/pkg/services/contexthandler/contexthandler_test.go index 3e61b65d0ed..fa882f2fc81 100644 --- a/pkg/services/contexthandler/contexthandler_test.go +++ b/pkg/services/contexthandler/contexthandler_test.go @@ -7,14 +7,16 @@ import ( "net/http/httptest" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/auth" + "github.com/grafana/grafana/pkg/services/auth/authtest" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/web" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestDontRotateTokensOnCancelledRequests(t *testing.T) { @@ -25,15 +27,15 @@ func TestDontRotateTokensOnCancelledRequests(t *testing.T) { require.NoError(t, err) tryRotateCallCount := 0 - uts := &auth.FakeUserAuthTokenService{ - TryRotateTokenProvider: func(ctx context.Context, token *models.UserToken, clientIP net.IP, + uts := &authtest.FakeUserAuthTokenService{ + TryRotateTokenProvider: func(ctx context.Context, token *auth.UserToken, clientIP net.IP, userAgent string) (bool, error) { tryRotateCallCount++ return false, nil }, } - token := &models.UserToken{AuthToken: "oldtoken"} + token := &auth.UserToken{AuthToken: "oldtoken"} fn := ctxHdlr.rotateEndOfRequestFunc(reqContext, uts, token) cancel() @@ -48,8 +50,8 @@ func TestTokenRotationAtEndOfRequest(t *testing.T) { reqContext, rr, err := initTokenRotationScenario(context.Background(), t, ctxHdlr) require.NoError(t, err) - uts := &auth.FakeUserAuthTokenService{ - TryRotateTokenProvider: func(ctx context.Context, token *models.UserToken, clientIP net.IP, + uts := &authtest.FakeUserAuthTokenService{ + TryRotateTokenProvider: func(ctx context.Context, token *auth.UserToken, clientIP net.IP, userAgent string) (bool, error) { newToken, err := util.RandomHex(16) require.NoError(t, err) @@ -58,7 +60,7 @@ func TestTokenRotationAtEndOfRequest(t *testing.T) { }, } - token := &models.UserToken{AuthToken: "oldtoken"} + token := &auth.UserToken{AuthToken: "oldtoken"} ctxHdlr.rotateEndOfRequestFunc(reqContext, uts, token)(reqContext.Resp) diff --git a/pkg/services/ngalert/api/util_test.go b/pkg/services/ngalert/api/util_test.go index 34f4deb08e2..3ed32d797e6 100644 --- a/pkg/services/ngalert/api/util_test.go +++ b/pkg/services/ngalert/api/util_test.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" + "github.com/grafana/grafana/pkg/services/auth" models2 "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" @@ -45,7 +46,7 @@ func TestAlertingProxy_createProxyContext(t *testing.T) { Req: &http.Request{}, }, SignedInUser: &user.SignedInUser{}, - UserToken: &models.UserToken{}, + UserToken: &auth.UserToken{}, IsSignedIn: rand.Int63()%2 == 1, IsRenderCall: rand.Int63()%2 == 1, AllowAnonymous: rand.Int63()%2 == 1, diff --git a/pkg/services/quota/quotaimpl/quota_test.go b/pkg/services/quota/quotaimpl/quota_test.go index 17164adc785..5d73021f2c9 100644 --- a/pkg/services/quota/quotaimpl/quota_test.go +++ b/pkg/services/quota/quotaimpl/quota_test.go @@ -14,6 +14,7 @@ import ( "github.com/grafana/grafana/pkg/services/apikey" "github.com/grafana/grafana/pkg/services/apikey/apikeyimpl" "github.com/grafana/grafana/pkg/services/auth" + "github.com/grafana/grafana/pkg/services/auth/authimpl" "github.com/grafana/grafana/pkg/services/dashboards" dashboardStore "github.com/grafana/grafana/pkg/services/dashboards/database" "github.com/grafana/grafana/pkg/services/datasources" @@ -464,7 +465,7 @@ func getQuotaBySrvTargetScope(t *testing.T, quotaService quota.Service, srv quot func setupEnv(t *testing.T, sqlStore *sqlstore.SQLStore, b bus.Bus, quotaService quota.Service) { _, err := apikeyimpl.ProvideService(sqlStore, sqlStore.Cfg, quotaService) require.NoError(t, err) - _, err = auth.ProvideActiveAuthTokenService(sqlStore.Cfg, sqlStore, quotaService) + _, err = authimpl.ProvideActiveAuthTokenService(sqlStore.Cfg, sqlStore, quotaService) require.NoError(t, err) _, err = dashboardStore.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService) require.NoError(t, err) From 48c34d310c244be32b56d3cd6db530ea390ce726 Mon Sep 17 00:00:00 2001 From: George Robinson Date: Fri, 18 Nov 2022 09:04:43 +0000 Subject: [PATCH 003/153] Alerting: Add tests that check current No Data behaviour with two conditions (#58650) --- pkg/expr/classic/classic_test.go | 125 +++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/pkg/expr/classic/classic_test.go b/pkg/expr/classic/classic_test.go index e5d85f9dd78..74f6a09eda2 100644 --- a/pkg/expr/classic/classic_test.go +++ b/pkg/expr/classic/classic_test.go @@ -473,6 +473,131 @@ func TestConditionsCmd(t *testing.T) { }) return newResults(v) }, + }, { + name: "two queries with two conditions using and operator and first is No Data", + vars: mathexp.Vars{ + "A": mathexp.Results{ + Values: []mathexp.Value{mathexp.NoData{}.New()}, + }, + "B": mathexp.Results{ + Values: []mathexp.Value{newSeries(ptr.Float64(5))}, + }, + }, + cmd: &ConditionsCmd{ + Conditions: []condition{ + { + InputRefID: "A", + Reducer: reducer("min"), + Operator: "and", + Evaluator: &thresholdEvaluator{"gt", 1}, + }, + { + InputRefID: "B", + Reducer: reducer("min"), + Operator: "and", + Evaluator: &thresholdEvaluator{"gt", 1}, + }, + }, + }, + expected: func() mathexp.Results { + v := newNumber(ptr.Float64(0)) + v.SetMeta([]EvalMatch{{Metric: "NoData"}, {Value: ptr.Float64(5)}}) + return newResults(v) + }, + }, { + // TODO: NoData behavior is different if the last condition is no data + name: "two queries with two conditions using and operator and last is No Data", + vars: mathexp.Vars{ + "A": mathexp.Results{ + Values: []mathexp.Value{newSeries(ptr.Float64(5))}, + }, + "B": mathexp.Results{ + Values: []mathexp.Value{mathexp.NoData{}.New()}, + }, + }, + cmd: &ConditionsCmd{ + Conditions: []condition{ + { + InputRefID: "A", + Reducer: reducer("min"), + Operator: "and", + Evaluator: &thresholdEvaluator{"gt", 1}, + }, + { + InputRefID: "B", + Reducer: reducer("min"), + Operator: "and", + Evaluator: &thresholdEvaluator{"gt", 1}, + }, + }, + }, + expected: func() mathexp.Results { + v := newNumber(nil) + v.SetMeta([]EvalMatch{{Value: ptr.Float64(5)}, {Metric: "NoData"}}) + return newResults(v) + }, + }, { + name: "two queries with two conditions using or operator and first is No Data", + vars: mathexp.Vars{ + "A": mathexp.Results{ + Values: []mathexp.Value{mathexp.NoData{}.New()}, + }, + "B": mathexp.Results{ + Values: []mathexp.Value{newSeries(ptr.Float64(5))}, + }, + }, + cmd: &ConditionsCmd{ + Conditions: []condition{ + { + InputRefID: "A", + Reducer: reducer("min"), + Operator: "or", + Evaluator: &thresholdEvaluator{"gt", 1}, + }, + { + InputRefID: "B", + Reducer: reducer("min"), + Operator: "or", + Evaluator: &thresholdEvaluator{"gt", 1}, + }, + }, + }, + expected: func() mathexp.Results { + v := newNumber(nil) + v.SetMeta([]EvalMatch{{Metric: "NoData"}, {Value: ptr.Float64(5)}}) + return newResults(v) + }, + }, { + name: "two queries with two conditions using or operator and last is No Data", + vars: mathexp.Vars{ + "A": mathexp.Results{ + Values: []mathexp.Value{newSeries(ptr.Float64(5))}, + }, + "B": mathexp.Results{ + Values: []mathexp.Value{mathexp.NoData{}.New()}, + }, + }, + cmd: &ConditionsCmd{ + Conditions: []condition{ + { + InputRefID: "A", + Reducer: reducer("min"), + Operator: "or", + Evaluator: &thresholdEvaluator{"gt", 1}, + }, + { + InputRefID: "B", + Reducer: reducer("min"), + Operator: "or", + Evaluator: &thresholdEvaluator{"gt", 1}, + }, + }, + }, + expected: func() mathexp.Results { + v := newNumber(nil) + v.SetMeta([]EvalMatch{{Value: ptr.Float64(5)}, {Metric: "NoData"}}) + return newResults(v) + }, }} for _, tt := range tests { From d46e3916a137aed51c917c50c54b8fb969b829e0 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Fri, 18 Nov 2022 09:05:45 +0000 Subject: [PATCH 004/153] Navigation: move connections + integrations to be a top level item (#58902) * move connections + integrations to be a top level item * add a test to check we can move apps to the root * split out movePlugin logic into a separate function * fix linting * rename movePlugin -> addPluginToSection --- pkg/services/navtree/models.go | 1 + pkg/services/navtree/navtreeimpl/applinks.go | 13 +++++++--- .../navtree/navtreeimpl/applinks_test.go | 24 +++++++++++++++++++ .../app/core/components/MegaMenu/MegaMenu.tsx | 7 ++---- 4 files changed, 37 insertions(+), 8 deletions(-) diff --git a/pkg/services/navtree/models.go b/pkg/services/navtree/models.go index f71ceea0808..8f521be5a46 100644 --- a/pkg/services/navtree/models.go +++ b/pkg/services/navtree/models.go @@ -36,6 +36,7 @@ const ( ) const ( + NavIDRoot = "root" NavIDDashboards = "dashboards" NavIDDashboardsBrowse = "dashboards/browse" NavIDCfg = "cfg" // NavIDCfg is the id for org configuration navigation node diff --git a/pkg/services/navtree/navtreeimpl/applinks.go b/pkg/services/navtree/navtreeimpl/applinks.go index 22c0aef5acf..e59a4d6bd09 100644 --- a/pkg/services/navtree/navtreeimpl/applinks.go +++ b/pkg/services/navtree/navtreeimpl/applinks.go @@ -170,6 +170,12 @@ func (s *ServiceImpl) processAppPlugin(plugin plugins.PluginDTO, c *models.ReqCo } appLink.Children = childrenWithoutDefault + s.addPluginToSection(c, treeRoot, plugin, appLink) + + return nil +} + +func (s *ServiceImpl) addPluginToSection(c *models.ReqContext, treeRoot *navtree.NavTreeRoot, plugin plugins.PluginDTO, appLink *navtree.NavLink) { // Handle moving apps into specific navtree sections alertingNode := treeRoot.FindById(navtree.NavIDAlerting) sectionID := navtree.NavIDApps @@ -183,7 +189,9 @@ func (s *ServiceImpl) processAppPlugin(plugin plugins.PluginDTO, c *models.ReqCo } } - if navNode := treeRoot.FindById(sectionID); navNode != nil { + if sectionID == navtree.NavIDRoot { + treeRoot.AddSection(appLink) + } else if navNode := treeRoot.FindById(sectionID); navNode != nil { navNode.Children = append(navNode.Children, appLink) } else { switch sectionID { @@ -227,8 +235,6 @@ func (s *ServiceImpl) processAppPlugin(plugin plugins.PluginDTO, c *models.ReqCo s.log.Error("Plugin app nav id not found", "pluginId", plugin.ID, "navId", sectionID) } } - - return nil } func (s *ServiceImpl) hasAccessToInclude(c *models.ReqContext, pluginID string) func(include *plugins.Includes) bool { @@ -256,6 +262,7 @@ func (s *ServiceImpl) readNavigationSettings() { "grafana-incident-app": {SectionID: navtree.NavIDAlertsAndIncidents, SortWeight: 2, Text: "Incident"}, "grafana-ml-app": {SectionID: navtree.NavIDAlertsAndIncidents, SortWeight: 3, Text: "Machine Learning"}, "grafana-cloud-link-app": {SectionID: navtree.NavIDCfg}, + "grafana-easystart-app": {SectionID: navtree.NavIDRoot, SortWeight: navtree.WeightSavedItems + 1, Text: "Connections"}, } s.navigationAppPathConfig = map[string]NavigationAppConfig{ diff --git a/pkg/services/navtree/navtreeimpl/applinks_test.go b/pkg/services/navtree/navtreeimpl/applinks_test.go index de6ffacf500..f0ed89967ba 100644 --- a/pkg/services/navtree/navtreeimpl/applinks_test.go +++ b/pkg/services/navtree/navtreeimpl/applinks_test.go @@ -147,6 +147,30 @@ func TestAddAppLinks(t *testing.T) { require.Equal(t, "Page2", app1Node.Children[0].Text) }) + // This can be done by using `[navigation.app_sections]` in the INI config + t.Run("Should move apps that have root nav id configured to the root", func(t *testing.T) { + service.features = featuremgmt.WithFeatures(featuremgmt.FlagTopnav) + service.navigationAppConfig = map[string]NavigationAppConfig{ + "test-app1": {SectionID: navtree.NavIDRoot}, + } + + treeRoot := navtree.NavTreeRoot{} + + err := service.addAppLinks(&treeRoot, reqCtx) + require.NoError(t, err) + + // Check if the plugin gets moved to the root + require.Len(t, treeRoot.Children, 2) + require.Equal(t, "plugin-page-test-app1", treeRoot.Children[0].Id) + + // Check if it is not under the "Apps" section anymore + appsNode := treeRoot.FindById(navtree.NavIDApps) + require.NotNil(t, appsNode) + require.Len(t, appsNode.Children, 2) + require.Equal(t, "plugin-page-test-app2", appsNode.Children[0].Id) + require.Equal(t, "plugin-page-test-app3", appsNode.Children[1].Id) + }) + // This can be done by using `[navigation.app_sections]` in the INI config t.Run("Should move apps that have specific nav id configured to correct section", func(t *testing.T) { service.features = featuremgmt.WithFeatures(featuremgmt.FlagTopnav) diff --git a/public/app/core/components/MegaMenu/MegaMenu.tsx b/public/app/core/components/MegaMenu/MegaMenu.tsx index a6115e7988f..b9a4cef37c7 100644 --- a/public/app/core/components/MegaMenu/MegaMenu.tsx +++ b/public/app/core/components/MegaMenu/MegaMenu.tsx @@ -25,17 +25,14 @@ export const MegaMenu = React.memo(({ onClose, searchBarHidden }) => { const navTree = cloneDeep(navBarTree); const coreItems = navTree - .filter((item) => item.section === NavSection.Core) - .map((item) => enrichWithInteractionTracking(item, true)); - const pluginItems = navTree - .filter((item) => item.section === NavSection.Plugin) + .filter((item) => item.section === NavSection.Core || item.section === NavSection.Plugin) .map((item) => enrichWithInteractionTracking(item, true)); const configItems = enrichConfigItems( navTree.filter((item) => item.section === NavSection.Config && item && item.id !== 'help' && item.id !== 'profile'), location ).map((item) => enrichWithInteractionTracking(item, true)); - const navItems = [...coreItems, ...pluginItems, ...configItems]; + const navItems = [...coreItems, ...configItems]; const activeItem = getActiveItem(navItems, location.pathname); From 9c98314e9f9f488470eea88d6f596d7d4cf9ade3 Mon Sep 17 00:00:00 2001 From: Misi Date: Fri, 18 Nov 2022 10:12:17 +0100 Subject: [PATCH 005/153] OAuth: Refactor OAuth parameters handling to support obtaining refresh tokens for Google OAuth (#58782) * Add ApprovalForce to AuthCodeOptions * Extract access token validity check to a function * Refactor * Oauth: set options internally instead of exposing new function * Align tests * Remove unused function Co-authored-by: Karl Persson --- pkg/api/frontendsettings_test.go | 2 +- pkg/api/login_oauth.go | 4 +-- pkg/api/login_oauth_test.go | 8 +++--- pkg/login/social/azuread_oauth_test.go | 28 ++++++++++--------- pkg/login/social/generic_oauth.go | 9 ++++++ pkg/login/social/github_oauth_test.go | 4 ++- pkg/login/social/google_oauth.go | 9 ++++++ pkg/login/social/social.go | 21 ++++++++------ pkg/server/server.go | 2 +- pkg/services/contexthandler/contexthandler.go | 23 +++++++++------ 10 files changed, 70 insertions(+), 40 deletions(-) diff --git a/pkg/api/frontendsettings_test.go b/pkg/api/frontendsettings_test.go index ef37a076523..dc1d1446a43 100644 --- a/pkg/api/frontendsettings_test.go +++ b/pkg/api/frontendsettings_test.go @@ -58,7 +58,7 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features *featuremgmt. grafanaUpdateChecker: &updatechecker.GrafanaService{}, AccessControl: accesscontrolmock.New().WithDisabled(), PluginSettings: pluginSettings.ProvideService(sqlStore, secretsService), - SocialService: social.ProvideService(cfg), + SocialService: social.ProvideService(cfg, features), } m := web.New() diff --git a/pkg/api/login_oauth.go b/pkg/api/login_oauth.go index 88a465d7fdb..603f158611a 100644 --- a/pkg/api/login_oauth.go +++ b/pkg/api/login_oauth.go @@ -97,9 +97,7 @@ func (hs *HTTPServer) OAuthLogin(ctx *models.ReqContext) { code := ctx.Query("code") if code == "" { - // FIXME: access_type is a Google OAuth2 specific thing, consider refactoring this and moving to google_oauth.go - opts := []oauth2.AuthCodeOption{oauth2.AccessTypeOffline} - + var opts []oauth2.AuthCodeOption if provider.UsePKCE { ascii, pkce, err := genPKCECode() if err != nil { diff --git a/pkg/api/login_oauth_test.go b/pkg/api/login_oauth_test.go index b5143949443..f39c5cda96c 100644 --- a/pkg/api/login_oauth_test.go +++ b/pkg/api/login_oauth_test.go @@ -9,15 +9,15 @@ import ( "path/filepath" "testing" - "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/services/secrets/fakes" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/login/social" + "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/hooks" "github.com/grafana/grafana/pkg/services/licensing" + "github.com/grafana/grafana/pkg/services/secrets/fakes" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" ) @@ -36,7 +36,7 @@ func setupOAuthTest(t *testing.T, cfg *setting.Cfg) *web.Mux { Cfg: cfg, License: &licensing.OSSLicensingService{Cfg: cfg}, SQLStore: sqlStore, - SocialService: social.ProvideService(cfg), + SocialService: social.ProvideService(cfg, featuremgmt.WithFeatures()), HooksService: hooks.ProvideService(), SecretsService: fakes.NewFakeSecretsService(), } diff --git a/pkg/login/social/azuread_oauth_test.go b/pkg/login/social/azuread_oauth_test.go index ea632e9457b..4a03dc12002 100644 --- a/pkg/login/social/azuread_oauth_test.go +++ b/pkg/login/social/azuread_oauth_test.go @@ -13,6 +13,8 @@ import ( "golang.org/x/oauth2" "gopkg.in/square/go-jose.v2" "gopkg.in/square/go-jose.v2/jwt" + + "github.com/grafana/grafana/pkg/services/featuremgmt" ) func trueBoolPtr() *bool { @@ -54,7 +56,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) { ID: "1234", }, fields: fields{ - SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "Viewer", false), + SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "Viewer", false, *featuremgmt.WithFeatures()), }, want: &BasicUserInfo{ Id: "1234", @@ -93,7 +95,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) { ID: "1234", }, fields: fields{ - SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "Viewer", false), + SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "Viewer", false, *featuremgmt.WithFeatures()), }, want: &BasicUserInfo{ Id: "1234", @@ -143,7 +145,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) { { name: "Only other roles", fields: fields{ - SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "Viewer", false), + SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "Viewer", false, *featuremgmt.WithFeatures()), }, claims: &azureClaims{ Email: "me@example.com", @@ -171,7 +173,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) { ID: "1234", }, fields: fields{ - SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "Editor", false), + SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "Editor", false, *featuremgmt.WithFeatures()), }, want: &BasicUserInfo{ Id: "1234", @@ -220,7 +222,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) { }, { name: "Grafana Admin but setting is disabled", - fields: fields{SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{AllowAssignGrafanaAdmin: false}, "Editor", false)}, + fields: fields{SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{AllowAssignGrafanaAdmin: false}, "Editor", false, *featuremgmt.WithFeatures())}, claims: &azureClaims{ Email: "me@example.com", PreferredUsername: "", @@ -242,7 +244,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) { name: "Editor roles in claim and GrafanaAdminAssignment enabled", fields: fields{ SocialBase: newSocialBase("azuread", - &oauth2.Config{}, &OAuthInfo{AllowAssignGrafanaAdmin: true}, "", false)}, + &oauth2.Config{}, &OAuthInfo{AllowAssignGrafanaAdmin: true}, "", false, *featuremgmt.WithFeatures())}, claims: &azureClaims{ Email: "me@example.com", PreferredUsername: "", @@ -263,7 +265,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) { { name: "Grafana Admin and Editor roles in claim", fields: fields{SocialBase: newSocialBase("azuread", - &oauth2.Config{}, &OAuthInfo{AllowAssignGrafanaAdmin: true}, "", false)}, + &oauth2.Config{}, &OAuthInfo{AllowAssignGrafanaAdmin: true}, "", false, *featuremgmt.WithFeatures())}, claims: &azureClaims{ Email: "me@example.com", PreferredUsername: "", @@ -302,7 +304,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) { fields: fields{ allowedGroups: []string{"foo", "bar"}, SocialBase: newSocialBase("azuread", - &oauth2.Config{}, &OAuthInfo{AllowAssignGrafanaAdmin: false}, "Viewer", false), + &oauth2.Config{}, &OAuthInfo{AllowAssignGrafanaAdmin: false}, "Viewer", false, *featuremgmt.WithFeatures()), }, claims: &azureClaims{ Email: "me@example.com", @@ -324,7 +326,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) { { name: "Fetch groups when ClaimsNames and ClaimsSources is set", fields: fields{ - SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "", false), + SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "", false, *featuremgmt.WithFeatures()), }, claims: &azureClaims{ ID: "1", @@ -349,7 +351,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) { { name: "Fetch groups when forceUseGraphAPI is set", fields: fields{ - SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "", false), + SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "", false, *featuremgmt.WithFeatures()), forceUseGraphAPI: true, }, claims: &azureClaims{ @@ -376,7 +378,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) { { name: "Fetch empty role when strict attribute role is true and no match", fields: fields{ - SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{RoleAttributeStrict: true}, "", false), + SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{RoleAttributeStrict: true}, "", false, *featuremgmt.WithFeatures()), }, claims: &azureClaims{ Email: "me@example.com", @@ -392,7 +394,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) { { name: "Fetch empty role when strict attribute role is true and no role claims returned", fields: fields{ - SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{RoleAttributeStrict: true}, "", false), + SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{RoleAttributeStrict: true}, "", false, *featuremgmt.WithFeatures()), }, claims: &azureClaims{ Email: "me@example.com", @@ -416,7 +418,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) { } if tt.fields.SocialBase == nil { - s.SocialBase = newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "", false) + s.SocialBase = newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "", false, *featuremgmt.WithFeatures()) } key := []byte("secret") diff --git a/pkg/login/social/generic_oauth.go b/pkg/login/social/generic_oauth.go index 69a47ccef22..d0cd1bc716d 100644 --- a/pkg/login/social/generic_oauth.go +++ b/pkg/login/social/generic_oauth.go @@ -14,6 +14,8 @@ import ( "strconv" "golang.org/x/oauth2" + + "github.com/grafana/grafana/pkg/services/featuremgmt" ) type SocialGenericOAuth struct { @@ -504,3 +506,10 @@ func (s *SocialGenericOAuth) FetchOrganizations(client *http.Client) ([]string, return logins, true } + +func (s *SocialGenericOAuth) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string { + if s.features.IsEnabled(featuremgmt.FlagAccessTokenExpirationCheck) { + opts = append(opts, oauth2.AccessTypeOffline) + } + return s.SocialBase.AuthCodeURL(state, opts...) +} diff --git a/pkg/login/social/github_oauth_test.go b/pkg/login/social/github_oauth_test.go index f610bd5843c..cdb400b15ea 100644 --- a/pkg/login/social/github_oauth_test.go +++ b/pkg/login/social/github_oauth_test.go @@ -9,6 +9,8 @@ import ( "github.com/stretchr/testify/require" "golang.org/x/oauth2" + + "github.com/grafana/grafana/pkg/services/featuremgmt" ) const testGHUserTeamsJSON = `[ @@ -202,7 +204,7 @@ func TestSocialGitHub_UserInfo(t *testing.T) { s := &SocialGithub{ SocialBase: newSocialBase("github", &oauth2.Config{}, - &OAuthInfo{RoleAttributePath: tt.roleAttributePath}, tt.autoAssignOrgRole, false), + &OAuthInfo{RoleAttributePath: tt.roleAttributePath}, tt.autoAssignOrgRole, false, *featuremgmt.WithFeatures()), allowedOrganizations: []string{}, apiUrl: server.URL + "/user", teamIds: []int{}, diff --git a/pkg/login/social/google_oauth.go b/pkg/login/social/google_oauth.go index 0c0a1d256dd..b499cc613be 100644 --- a/pkg/login/social/google_oauth.go +++ b/pkg/login/social/google_oauth.go @@ -6,6 +6,8 @@ import ( "net/http" "golang.org/x/oauth2" + + "github.com/grafana/grafana/pkg/services/featuremgmt" ) type SocialGoogle struct { @@ -38,3 +40,10 @@ func (s *SocialGoogle) UserInfo(client *http.Client, token *oauth2.Token) (*Basi Login: data.Email, }, nil } + +func (s *SocialGoogle) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string { + if s.features.IsEnabled(featuremgmt.FlagAccessTokenExpirationCheck) { + opts = append(opts, oauth2.AccessTypeOffline, oauth2.ApprovalForce) + } + return s.SocialBase.AuthCodeURL(state, opts...) +} diff --git a/pkg/login/social/social.go b/pkg/login/social/social.go index 217f2606a19..fad3ed7095b 100644 --- a/pkg/login/social/social.go +++ b/pkg/login/social/social.go @@ -16,6 +16,7 @@ import ( "golang.org/x/text/language" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" @@ -58,7 +59,7 @@ type OAuthInfo struct { UsePKCE bool } -func ProvideService(cfg *setting.Cfg) *SocialService { +func ProvideService(cfg *setting.Cfg, features *featuremgmt.FeatureManager) *SocialService { ss := SocialService{ cfg: cfg, oAuthProvider: make(map[string]*OAuthInfo), @@ -139,7 +140,7 @@ func ProvideService(cfg *setting.Cfg) *SocialService { // GitHub. if name == "github" { ss.socialMap["github"] = &SocialGithub{ - SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync), + SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features), apiUrl: info.ApiUrl, teamIds: sec.Key("team_ids").Ints(","), allowedOrganizations: util.SplitString(sec.Key("allowed_organizations").String()), @@ -149,7 +150,7 @@ func ProvideService(cfg *setting.Cfg) *SocialService { // GitLab. if name == "gitlab" { ss.socialMap["gitlab"] = &SocialGitlab{ - SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync), + SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features), apiUrl: info.ApiUrl, allowedGroups: util.SplitString(sec.Key("allowed_groups").String()), } @@ -158,7 +159,7 @@ func ProvideService(cfg *setting.Cfg) *SocialService { // Google. if name == "google" { ss.socialMap["google"] = &SocialGoogle{ - SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync), + SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features), hostedDomain: info.HostedDomain, apiUrl: info.ApiUrl, } @@ -167,7 +168,7 @@ func ProvideService(cfg *setting.Cfg) *SocialService { // AzureAD. if name == "azuread" { ss.socialMap["azuread"] = &SocialAzureAD{ - SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync), + SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features), allowedGroups: util.SplitString(sec.Key("allowed_groups").String()), forceUseGraphAPI: sec.Key("force_use_graph_api").MustBool(false), } @@ -176,7 +177,7 @@ func ProvideService(cfg *setting.Cfg) *SocialService { // Okta if name == "okta" { ss.socialMap["okta"] = &SocialOkta{ - SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync), + SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features), apiUrl: info.ApiUrl, allowedGroups: util.SplitString(sec.Key("allowed_groups").String()), } @@ -185,7 +186,7 @@ func ProvideService(cfg *setting.Cfg) *SocialService { // Generic - Uses the same scheme as GitHub. if name == "generic_oauth" { ss.socialMap["generic_oauth"] = &SocialGenericOAuth{ - SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync), + SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features), apiUrl: info.ApiUrl, teamsUrl: info.TeamsUrl, emailAttributeName: info.EmailAttributeName, @@ -214,8 +215,7 @@ func ProvideService(cfg *setting.Cfg) *SocialService { } ss.socialMap[grafanaCom] = &SocialGrafanaCom{ - SocialBase: newSocialBase(name, &config, info, - cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync), + SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features), url: cfg.GrafanaComURL, allowedOrganizations: util.SplitString(sec.Key("allowed_organizations").String()), } @@ -261,6 +261,7 @@ type SocialBase struct { roleAttributeStrict bool autoAssignOrgRole string skipOrgRoleSync bool + features featuremgmt.FeatureManager } type Error struct { @@ -295,6 +296,7 @@ func newSocialBase(name string, info *OAuthInfo, autoAssignOrgRole string, skipOrgRoleSync bool, + features featuremgmt.FeatureManager, ) *SocialBase { logger := log.New("oauth." + name) @@ -308,6 +310,7 @@ func newSocialBase(name string, roleAttributePath: info.RoleAttributePath, roleAttributeStrict: info.RoleAttributeStrict, skipOrgRoleSync: skipOrgRoleSync, + features: features, } } diff --git a/pkg/server/server.go b/pkg/server/server.go index aec724df979..4b80055b009 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -127,7 +127,7 @@ func (s *Server) init() error { } login.ProvideService(s.HTTPServer.SQLStore, s.HTTPServer.Login, s.loginAttemptService, s.userService) - social.ProvideService(s.cfg) + social.ProvideService(s.cfg, s.HTTPServer.Features) if err := s.roleRegistry.RegisterFixedRoles(s.context); err != nil { return err diff --git a/pkg/services/contexthandler/contexthandler.go b/pkg/services/contexthandler/contexthandler.go index 6e2be2233c5..01e48445926 100644 --- a/pkg/services/contexthandler/contexthandler.go +++ b/pkg/services/contexthandler/contexthandler.go @@ -449,20 +449,14 @@ func (h *ContextHandler) initContextWithToken(reqContext *models.ReqContext, org return false } - getTime := h.GetTime - if getTime == nil { - getTime = time.Now - } - if h.features.IsEnabled(featuremgmt.FlagAccessTokenExpirationCheck) { // Check whether the logged in User has a token (whether the User used an OAuth provider to login) oauthToken, exists, _ := h.oauthTokenService.HasOAuthEntry(ctx, queryResult) if exists { - // Skip where the OAuthExpiry is default/zero/unset - if !oauthToken.OAuthExpiry.IsZero() && oauthToken.OAuthExpiry.Round(0).Add(-oauthtoken.ExpiryDelta).Before(getTime()) { + if h.hasAccessTokenExpired(oauthToken) { reqContext.Logger.Info("access token expired", "userId", query.UserID, "expiry", fmt.Sprintf("%v", oauthToken.OAuthExpiry)) - // If the User doesn't have a refresh_token or refreshing the token was unsuccessful then log out the User and Invalidate the OAuth tokens + // If the User doesn't have a refresh_token or refreshing the token was unsuccessful then log out the User and invalidate the OAuth tokens if err = h.oauthTokenService.TryTokenRefresh(ctx, oauthToken); err != nil { if !errors.Is(err, oauthtoken.ErrNoRefreshTokenFound) { reqContext.Logger.Error("could not fetch a new access token", "userId", oauthToken.UserId, "error", err) @@ -732,3 +726,16 @@ func AuthHTTPHeaderListFromContext(c context.Context) *AuthHTTPHeaderList { } return nil } + +func (h *ContextHandler) hasAccessTokenExpired(token *models.UserAuth) bool { + if token.OAuthExpiry.IsZero() { + return false + } + + getTime := h.GetTime + if getTime == nil { + getTime = time.Now + } + + return token.OAuthExpiry.Round(0).Add(-oauthtoken.ExpiryDelta).Before(getTime()) +} From b77c3946a5b2dac7ded143b8caddfc33852182ef Mon Sep 17 00:00:00 2001 From: George Robinson Date: Fri, 18 Nov 2022 09:28:21 +0000 Subject: [PATCH 006/153] Alerting: Fix ConditionsCmd No Data for "has no value" (#58634) This commit fixes a bug where ConditionsCmd returns No Data even when the condition checks for "has no value". It should return 1 with a nil match. --- pkg/expr/classic/classic.go | 12 ++++++++---- pkg/expr/classic/classic_test.go | 13 ++++++------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/pkg/expr/classic/classic.go b/pkg/expr/classic/classic.go index 4b06f0ce65b..33e57d5e6a5 100644 --- a/pkg/expr/classic/classic.go +++ b/pkg/expr/classic/classic.go @@ -79,6 +79,12 @@ func (cmd *ConditionsCmd) Execute(_ context.Context, _ time.Time, vars mathexp.V querySeriesSet := vars[c.InputRefID] nilReducedCount := 0 firingCount := 0 + + if len(querySeriesSet.Values) == 0 { + // Append a NoData data frame so "has no value" still works + querySeriesSet.Values = append(querySeriesSet.Values, mathexp.NoData{}.New()) + } + for _, val := range querySeriesSet.Values { var reducedNum mathexp.Number var name string @@ -103,10 +109,6 @@ func (cmd *ConditionsCmd) Execute(_ context.Context, _ time.Time, vars mathexp.V // TODO handle error / no data signals thisCondNoDataFound := reducedNum.GetFloat64Value() == nil - if thisCondNoDataFound { - nilReducedCount++ - } - evalRes := c.Evaluator.Eval(reducedNum) if evalRes { @@ -119,6 +121,8 @@ func (cmd *ConditionsCmd) Execute(_ context.Context, _ time.Time, vars mathexp.V } matches = append(matches, match) firingCount++ + } else if thisCondNoDataFound { + nilReducedCount++ } } diff --git a/pkg/expr/classic/classic_test.go b/pkg/expr/classic/classic_test.go index 74f6a09eda2..266640fc1bc 100644 --- a/pkg/expr/classic/classic_test.go +++ b/pkg/expr/classic/classic_test.go @@ -203,9 +203,8 @@ func TestConditionsCmd(t *testing.T) { }, }, expected: func() mathexp.Results { - v := newNumber(nil) - // This seems incorrect - v.SetMeta([]EvalMatch{{}, {Metric: "NoData"}}) + v := newNumber(ptr.Float64(1)) + v.SetMeta([]EvalMatch{{Value: nil}}) return newResults(v) }, }, { @@ -226,9 +225,9 @@ func TestConditionsCmd(t *testing.T) { }, }, expected: func() mathexp.Results { - v := newNumber(nil) + v := newNumber(ptr.Float64(1)) // This too seems incorrect, looks like we don't call the evaluator - v.SetMeta([]EvalMatch{{Metric: "NoData"}}) + v.SetMeta([]EvalMatch{{Value: nil}}) return newResults(v) }, }, { @@ -251,9 +250,9 @@ func TestConditionsCmd(t *testing.T) { }, }, expected: func() mathexp.Results { - v := newNumber(nil) + v := newNumber(ptr.Float64(1)) // This seems incorrect - v.SetMeta([]EvalMatch{{}, {Metric: "NoData"}}) + v.SetMeta([]EvalMatch{{Value: nil}}) return newResults(v) }, }, { From 38b980bd818e005fa82c920867c37bbf99977820 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 18 Nov 2022 10:35:27 +0100 Subject: [PATCH 007/153] SceneObject: Prevent state mutation by using Object.freeze (#58936) --- .../features/scenes/core/SceneObjectBase.test.ts | 14 ++++++++++++++ .../app/features/scenes/core/SceneObjectBase.tsx | 10 ++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/public/app/features/scenes/core/SceneObjectBase.test.ts b/public/app/features/scenes/core/SceneObjectBase.test.ts index c8d3f138e76..e36d5479388 100644 --- a/public/app/features/scenes/core/SceneObjectBase.test.ts +++ b/public/app/features/scenes/core/SceneObjectBase.test.ts @@ -76,6 +76,20 @@ describe('SceneObject', () => { expect(clone.state.name).toBe('new name'); }); + it('Cannot modify state', () => { + const scene = new TestScene({ name: 'name' }); + expect(() => { + scene.state.name = 'new name'; + }).toThrow(); + + scene.setState({ name: 'new name' }); + expect(scene.state.name).toBe('new name'); + + expect(() => { + scene.state.name = 'other name'; + }).toThrow(); + }); + describe('When activated', () => { const scene = new TestScene({ $data: new SceneDataNode({}), diff --git a/public/app/features/scenes/core/SceneObjectBase.tsx b/public/app/features/scenes/core/SceneObjectBase.tsx index f0c5f75b27f..ffb997e5a1d 100644 --- a/public/app/features/scenes/core/SceneObjectBase.tsx +++ b/public/app/features/scenes/core/SceneObjectBase.tsx @@ -32,7 +32,7 @@ export abstract class SceneObjectBase) { const prevState = this._state; - this._state = { + const newState: TState = { ...this._state, ...update, }; + this._state = Object.freeze(newState); + this.setParent(); - this._subject.next(this._state); + this._subject.next(newState); // Bubble state change event. This is event is subscribed to by UrlSyncManager and UndoManager this.publishEvent( new SceneObjectStateChangedEvent({ prevState, - newState: this._state, + newState, partialUpdate: update, changedObject: this, }), From 0e4108f62f1e9a03a0097f2b8c1236582cac5cfd Mon Sep 17 00:00:00 2001 From: Alex Pakalniskis <43630382+alex-pakalniskis@users.noreply.github.com> Date: Fri, 18 Nov 2022 02:34:25 -0800 Subject: [PATCH 008/153] Documentation Update: Minor spelling change (#58933) chore: minor grammar tweak --- .../alerting/alerting-rules/create-grafana-managed-rule.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md b/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md index 67d9736dc13..786de9cf53b 100644 --- a/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md +++ b/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md @@ -15,7 +15,7 @@ weight: 400 # Create a Grafana managed alerting rule -Grafana allows you to create alerting rules that query one or more data sources, reduce or transform the results and compare them to each other or to fix thresholds. When these are executed, Grafana sends notifications to the contact point. For information on Grafana Alerting, see [About Grafana Alerting]({{< relref "../" >}}) which explains the various components of Grafana Alerting. We also recommend that you familiarize yourself with some of the [fundamental concepts]({{< relref "../fundamentals/" >}}) of Grafana Alerting. +Grafana allows you to create alerting rules that query one or more data sources, reduce or transform the results and compare them to each other or to fixed thresholds. When these are executed, Grafana sends notifications to the contact point. For information on Grafana Alerting, see [About Grafana Alerting]({{< relref "../" >}}) which explains the various components of Grafana Alerting. We also recommend that you familiarize yourself with some of the [fundamental concepts]({{< relref "../fundamentals/" >}}) of Grafana Alerting. Watch this video to learn more about creating alerts: {{< vimeo 720001934 >}} From 8e19a1618fd6547189ffc015cb5329e914e484ab Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Fri, 18 Nov 2022 10:46:50 +0000 Subject: [PATCH 009/153] QueryData: skip header validation (revert check) (#58871) --- pkg/services/query/query.go | 3 ++- pkg/services/query/query_test.go | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/services/query/query.go b/pkg/services/query/query.go index 773d0577f43..cefd648e19e 100644 --- a/pkg/services/query/query.go +++ b/pkg/services/query/query.go @@ -372,7 +372,8 @@ func (s *Service) parseMetricRequest(ctx context.Context, user *user.SignedInUse req.httpRequest = reqDTO.HTTPRequest } - return req, req.validateRequest() + _ = req.validateRequest() + return req, nil // TODO req.validateRequest() } func (s *Service) getDataSourceFromQuery(ctx context.Context, user *user.SignedInUser, skipCache bool, query *simplejson.Json, history map[string]*datasources.DataSource) (*datasources.DataSource, error) { diff --git a/pkg/services/query/query_test.go b/pkg/services/query/query_test.go index e8da4ab6481..af81ea8f8b4 100644 --- a/pkg/services/query/query_test.go +++ b/pkg/services/query/query_test.go @@ -189,7 +189,7 @@ func TestParseMetricRequest(t *testing.T) { httpreq.Header.Add("X-Datasource-Uid", "gIEkMvIVz") mr.HTTPRequest = httpreq _, err := tc.queryService.parseMetricRequest(context.Background(), tc.signedInUser, true, mr) - require.Error(t, err) + require.NoError(t, err) // With the second value it is OK httpreq.Header.Add("X-Datasource-Uid", "sEx6ZvSVk") From e823a90b82addd7350aa7241d2d5007d8c94ceb7 Mon Sep 17 00:00:00 2001 From: sfranzis Date: Fri, 18 Nov 2022 12:13:31 +0100 Subject: [PATCH 010/153] GaugePanel: Setting the neutral-point of a gauge (#53989) --- .../src/components/Gauge/Gauge.test.tsx | 3 + .../grafana-ui/src/components/Gauge/Gauge.tsx | 1 + public/app/plugins/panel/gauge/module.tsx | 15 +++- public/vendor/flot/jquery.flot.gauge.js | 78 +++++++++++++++++-- 4 files changed, 87 insertions(+), 10 deletions(-) diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx index 0a337707c36..00037a9872c 100644 --- a/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx @@ -19,6 +19,9 @@ const field: FieldConfig = { mode: ThresholdsMode.Absolute, steps: [{ value: -Infinity, color: '#7EB26D' }], }, + custom: { + neeutral: 0, + }, }; const props: Props = { diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.tsx index 39e7a80808e..1f54b72aff4 100644 --- a/packages/grafana-ui/src/components/Gauge/Gauge.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.tsx @@ -98,6 +98,7 @@ export class Gauge extends PureComponent { gauge: { min, max, + neutralValue: field.custom?.neutral, background: { color: backgroundColor }, border: { color: null }, shadow: { show: false }, diff --git a/public/app/plugins/panel/gauge/module.tsx b/public/app/plugins/panel/gauge/module.tsx index 3a36fd460a2..0910083e231 100644 --- a/public/app/plugins/panel/gauge/module.tsx +++ b/public/app/plugins/panel/gauge/module.tsx @@ -9,11 +9,22 @@ import { PanelOptions, defaultPanelOptions } from './models.gen'; import { GaugeSuggestionsSupplier } from './suggestions'; export const plugin = new PanelPlugin(GaugePanel) - .useFieldConfig() + .useFieldConfig({ + useCustomConfig: (builder) => { + builder.addNumberInput({ + path: 'neutral', + name: 'Neutral', + description: 'Leave empty to use Min as neutral point', + category: ['Gauge'], + settings: { + placeholder: 'auto', + }, + }); + }, + }) .setPanelOptions((builder) => { addStandardDataReduceOptions(builder); addOrientationOption(builder); - builder .addBooleanSwitch({ path: 'showThresholdLabels', diff --git a/public/vendor/flot/jquery.flot.gauge.js b/public/vendor/flot/jquery.flot.gauge.js index 760354cecd4..8c5c43a9213 100644 --- a/public/vendor/flot/jquery.flot.gauge.js +++ b/public/vendor/flot/jquery.flot.gauge.js @@ -325,9 +325,9 @@ */ Gauge.prototype.drawGauge = function(gaugeOptionsi, layout, cellLayout, label, data) { - var blur = gaugeOptionsi.gauge.shadow.show ? gaugeOptionsi.gauge.shadow.blur : 0; - + var color = getColor(gaugeOptionsi, data); + var angles = calculateAnglesForGauge(gaugeOptionsi, layout, data); // draw gauge frame drawArcWithShadow( @@ -343,19 +343,74 @@ blur); // draw gauge - var c1 = getColor(gaugeOptionsi, data); - var a2 = calculateAngle(gaugeOptionsi, layout, data); drawArcWithShadow( cellLayout.cx, // center x cellLayout.cy, // center y layout.radius - 1, layout.width - 2, - toRad(gaugeOptionsi.gauge.startAngle), - toRad(a2), - c1, // line color + toRad(angles.a1), + toRad(angles.a2), + color, 1, // line width - c1, // fill color + color, // fill color blur); + + if(gaugeOptionsi.gauge.neutralValue != null) { + drawZeroMarker(gaugeOptionsi, layout, cellLayout, color); + } + } + + /** + * Calcualte the angles for the gauge, depending on if there are + * negative numbers or not. + * + * @method calculateAnglesForGauge + * @param {Object} gaugeOptionsi the options of the gauge + * @param {Number} data the value of the gauge + * @returns {Object} + */ + function calculateAnglesForGauge(gaugeOptionsi, layout, data) { + let angles = {}; + var neutral = gaugeOptionsi.gauge.neutralValue; + + if (neutral != null) { + if (data < neutral) { + angles.a1 = calculateAngle(gaugeOptionsi, layout, data); + angles.a2 = calculateAngle(gaugeOptionsi, layout, neutral); + } else { + angles.a1 = calculateAngle(gaugeOptionsi, layout, neutral); + angles.a2 = calculateAngle(gaugeOptionsi, layout, data); + } + } else { + angles.a1 = gaugeOptionsi.gauge.startAngle; + angles.a2 = calculateAngle(gaugeOptionsi, layout, data); + } + + return angles; + } + + /** + * Draw zero marker for Gauge with negative values + * + * @method drawZeroMarker + * @param {Object} gaugeOptionsi the options of the gauge + * @param {Object} layout the layout properties + * @param {Object} cellLayout the cell layout properties + * @param {String} color line color + */ + function drawZeroMarker(gaugeOptionsi, layout, cellLayout, color) { + var diff = (gaugeOptionsi.gauge.max - gaugeOptionsi.gauge.min) / 600; + + drawArc(context, + cellLayout.cx, + cellLayout.cy, + layout.radius - 2, + layout.width - 4, + toRad(calculateAngle(gaugeOptionsi, layout, gaugeOptionsi.gauge.neutralValue-diff)), + toRad(calculateAngle(gaugeOptionsi, layout, gaugeOptionsi.gauge.neutralValue+diff)), + color, + 2, + gaugeOptionsi.gauge.background.color); } /** @@ -529,6 +584,13 @@ drawThresholdValue(gaugeOptionsi, layout, cellLayout, i + "_" + j, threshold.value, a); } } + + var neutral = gaugeOptionsi.gauge.neutralValue; + if (neutral != null && + neutral>gaugeOptionsi.gauge.min && + neutral Date: Fri, 18 Nov 2022 15:37:18 +0200 Subject: [PATCH 011/153] CI: Move `upload-cdn` subcommand from `grabpl` (#58957) Move upload-cdn from grabpl --- .drone.yml | 16 ++++---- pkg/build/cmd/main.go | 8 ++++ pkg/build/cmd/uploadcdn.go | 75 ++++++++++++++++++++++++++++++++++++ scripts/drone/steps/lib.star | 2 +- 4 files changed, 92 insertions(+), 9 deletions(-) create mode 100644 pkg/build/cmd/uploadcdn.go diff --git a/.drone.yml b/.drone.yml index 103256ecf2f..4b48aa009e0 100644 --- a/.drone.yml +++ b/.drone.yml @@ -1569,7 +1569,7 @@ steps: repo: - grafana/grafana - commands: - - ./bin/grabpl upload-cdn --edition oss + - ./bin/build upload-cdn --edition oss depends_on: - grafana-server environment: @@ -2135,7 +2135,7 @@ steps: event: - tag - commands: - - ./bin/grabpl upload-cdn --edition oss + - ./bin/build upload-cdn --edition oss depends_on: - grafana-server environment: @@ -2785,7 +2785,7 @@ steps: - success - failure - commands: - - ./bin/grabpl upload-cdn --edition enterprise + - ./bin/build upload-cdn --edition enterprise depends_on: - package environment: @@ -2826,7 +2826,7 @@ steps: image: grafana/build-container:1.6.4 name: package-enterprise2 - commands: - - ./bin/grabpl upload-cdn --edition enterprise2 + - ./bin/build upload-cdn --edition enterprise2 depends_on: - package-enterprise2 environment: @@ -4156,7 +4156,7 @@ steps: include: - packages/grafana-ui/** - commands: - - ./bin/grabpl upload-cdn --edition oss + - ./bin/build upload-cdn --edition oss depends_on: - grafana-server environment: @@ -4764,7 +4764,7 @@ steps: - success - failure - commands: - - ./bin/grabpl upload-cdn --edition enterprise + - ./bin/build upload-cdn --edition enterprise depends_on: - package environment: @@ -4812,7 +4812,7 @@ steps: image: grafana/build-container:1.6.4 name: package-enterprise2 - commands: - - ./bin/grabpl upload-cdn --edition enterprise2 + - ./bin/build upload-cdn --edition enterprise2 depends_on: - package-enterprise2 environment: @@ -5512,6 +5512,6 @@ kind: secret name: packages_secret_access_key --- kind: signature -hmac: 77ae647c9addfcd9966d462ca9967b85a87e18adfe0e9e0a2c6b1cf5d7f42493 +hmac: bdde811590573d22162d8305ced15080e8b20f2180b7491891c461427810a4b3 ... diff --git a/pkg/build/cmd/main.go b/pkg/build/cmd/main.go index 46d8ef8f32a..ba4f1f6eb0a 100644 --- a/pkg/build/cmd/main.go +++ b/pkg/build/cmd/main.go @@ -96,6 +96,14 @@ func main() { }, }, }, + { + Name: "upload-cdn", + Usage: "Upload public/* to a cdn bucket", + Action: UploadCDN, + Flags: []cli.Flag{ + &editionFlag, + }, + }, { Name: "shellcheck", Usage: "Run shellcheck on shell scripts", diff --git a/pkg/build/cmd/uploadcdn.go b/pkg/build/cmd/uploadcdn.go new file mode 100644 index 00000000000..e9c4b72acab --- /dev/null +++ b/pkg/build/cmd/uploadcdn.go @@ -0,0 +1,75 @@ +package main + +import ( + "fmt" + "log" + "os" + "path/filepath" + + "github.com/grafana/grafana/pkg/build/config" + "github.com/grafana/grafana/pkg/build/gcloud/storage" + "github.com/urfave/cli/v2" +) + +// UploadCDN implements the sub-command "upload-cdn". +func UploadCDN(c *cli.Context) error { + if c.NArg() > 0 { + if err := cli.ShowSubcommandHelp(c); err != nil { + return cli.NewExitError(err.Error(), 1) + } + return cli.NewExitError("", 1) + } + + metadata, err := GenerateMetadata(c) + if err != nil { + return err + } + + version := metadata.GrafanaVersion + if err != nil { + return cli.NewExitError(err.Error(), 1) + } + + buildConfig, err := config.GetBuildConfig(metadata.ReleaseMode.Mode) + if err != nil { + return err + } + + edition := os.Getenv("EDITION") + log.Printf("Uploading Grafana CDN Assets, version %s, %s edition...", version, edition) + + editionPath := "" + + switch config.Edition(edition) { + case config.EditionOSS: + editionPath = "grafana-oss" + case config.EditionEnterprise: + editionPath = "grafana" + case config.EditionEnterprise2: + editionPath = os.Getenv("ENTERPRISE2_CDN_PATH") + default: + panic(fmt.Sprintf("unrecognized edition %q", edition)) + } + + gcs, err := storage.New() + if err != nil { + return err + } + + bucket := gcs.Bucket(buildConfig.Buckets.CDNAssets) + srcPath := buildConfig.Buckets.CDNAssetsDir + srcPath = filepath.Join(srcPath, editionPath, version) + + if err := gcs.DeleteDir(c.Context, bucket, srcPath); err != nil { + return err + } + log.Printf("Successfully cleaned source: %s/%s\n", buildConfig.Buckets.CDNAssets, srcPath) + + if err := gcs.CopyLocalDir(c.Context, "./public", bucket, srcPath, false); err != nil { + return err + } + + log.Printf("Successfully uploaded cdn static assets to: %s/%s!\n", buildConfig.Buckets.CDNAssets, srcPath) + + return nil +} diff --git a/scripts/drone/steps/lib.star b/scripts/drone/steps/lib.star index d185b6fe567..bf45fc1a63d 100644 --- a/scripts/drone/steps/lib.star +++ b/scripts/drone/steps/lib.star @@ -354,7 +354,7 @@ def upload_cdn_step(edition, ver_mode, trigger=None): 'PRERELEASE_BUCKET': from_secret(prerelease_bucket) }, 'commands': [ - './bin/grabpl upload-cdn --edition {}'.format(edition), + './bin/build upload-cdn --edition {}'.format(edition), ], } if trigger and ver_mode in ("release-branch", "main"): From b3406a8273122cca702fd2e13c550bb1b0befc99 Mon Sep 17 00:00:00 2001 From: Karl Persson Date: Fri, 18 Nov 2022 14:40:26 +0100 Subject: [PATCH 012/153] Auth: Remove userauth service (#58941) * Auth: remove userauth service * Use Revoke user tokens from UserAuthTokenService * Add function to delete user auth info to UserAuthInfo service --- pkg/api/admin_users.go | 4 +- pkg/api/http_server.go | 6 +-- pkg/cmd/grafana-cli/runner/wire.go | 4 +- pkg/server/wire.go | 2 - pkg/services/login/authinfo.go | 1 + .../authinfoservice/database/database.go | 8 ++++ pkg/services/login/authinfoservice/service.go | 4 ++ pkg/services/login/logintest/logintest.go | 4 ++ pkg/services/userauth/userauth.go | 8 ---- pkg/services/userauth/userauthimpl/store.go | 32 ---------------- .../userauth/userauthimpl/store_test.go | 31 ---------------- .../userauth/userauthimpl/userauth.go | 28 -------------- .../userauth/userauthimpl/userauth_test.go | 37 ------------------- pkg/services/userauth/userauthtest/fake.go | 19 ---------- 14 files changed, 21 insertions(+), 167 deletions(-) delete mode 100644 pkg/services/userauth/userauth.go delete mode 100644 pkg/services/userauth/userauthimpl/store.go delete mode 100644 pkg/services/userauth/userauthimpl/store_test.go delete mode 100644 pkg/services/userauth/userauthimpl/userauth.go delete mode 100644 pkg/services/userauth/userauthimpl/userauth_test.go delete mode 100644 pkg/services/userauth/userauthtest/fake.go diff --git a/pkg/api/admin_users.go b/pkg/api/admin_users.go index bf284cddb33..54855e7d281 100644 --- a/pkg/api/admin_users.go +++ b/pkg/api/admin_users.go @@ -239,13 +239,13 @@ func (hs *HTTPServer) AdminDeleteUser(c *models.ReqContext) response.Response { return nil }) g.Go(func() error { - if err := hs.userAuthService.Delete(ctx, cmd.UserID); err != nil { + if err := hs.authInfoService.DeleteUserAuthInfo(ctx, cmd.UserID); err != nil { return err } return nil }) g.Go(func() error { - if err := hs.userAuthService.DeleteToken(ctx, cmd.UserID); err != nil { + if err := hs.AuthTokenService.RevokeAllUserTokens(ctx, cmd.UserID); err != nil { return err } return nil diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 7320b71b2cd..222b6923990 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -21,7 +21,6 @@ import ( "github.com/grafana/grafana/pkg/services/querylibrary" "github.com/grafana/grafana/pkg/services/searchV2" "github.com/grafana/grafana/pkg/services/store/object/httpobjectstore" - "github.com/grafana/grafana/pkg/services/userauth" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" @@ -207,7 +206,6 @@ type HTTPServer struct { accesscontrolService accesscontrol.Service annotationsRepo annotations.Repository tagService tag.Service - userAuthService userauth.Service oauthTokenService oauthtoken.OAuthTokenService } @@ -250,8 +248,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi loginAttemptService loginAttempt.Service, orgService org.Service, teamService team.Service, accesscontrolService accesscontrol.Service, dashboardThumbsService thumbs.DashboardThumbService, navTreeService navtree.Service, annotationRepo annotations.Repository, tagService tag.Service, searchv2HTTPService searchV2.SearchHTTPService, - userAuthService userauth.Service, queryLibraryHTTPService querylibrary.HTTPService, queryLibraryService querylibrary.Service, - oauthTokenService oauthtoken.OAuthTokenService, + queryLibraryHTTPService querylibrary.HTTPService, queryLibraryService querylibrary.Service, oauthTokenService oauthtoken.OAuthTokenService, ) (*HTTPServer, error) { web.Env = cfg.Env m := web.New() @@ -353,7 +350,6 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi accesscontrolService: accesscontrolService, annotationsRepo: annotationRepo, tagService: tagService, - userAuthService: userAuthService, QueryLibraryHTTPService: queryLibraryHTTPService, QueryLibraryService: queryLibraryService, oauthTokenService: oauthTokenService, diff --git a/pkg/cmd/grafana-cli/runner/wire.go b/pkg/cmd/grafana-cli/runner/wire.go index 8ee0af3f6a5..975264bba82 100644 --- a/pkg/cmd/grafana-cli/runner/wire.go +++ b/pkg/cmd/grafana-cli/runner/wire.go @@ -7,7 +7,6 @@ import ( "context" "github.com/google/wire" - "github.com/grafana/grafana/pkg/services/auth/authimpl" "github.com/grafana/grafana/pkg/tsdb/parca" "github.com/grafana/grafana/pkg/tsdb/phlare" @@ -53,6 +52,7 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol" "github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/auth" + "github.com/grafana/grafana/pkg/services/auth/authimpl" "github.com/grafana/grafana/pkg/services/auth/jwt" "github.com/grafana/grafana/pkg/services/cleanup" "github.com/grafana/grafana/pkg/services/comments" @@ -129,7 +129,6 @@ import ( "github.com/grafana/grafana/pkg/services/thumbs" "github.com/grafana/grafana/pkg/services/updatechecker" "github.com/grafana/grafana/pkg/services/user/userimpl" - "github.com/grafana/grafana/pkg/services/userauth/userauthimpl" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb/azuremonitor" "github.com/grafana/grafana/pkg/tsdb/cloudmonitoring" @@ -327,7 +326,6 @@ var wireSet = wire.NewSet( userimpl.ProvideService, orgimpl.ProvideService, teamimpl.ProvideService, - userauthimpl.ProvideService, ngmetrics.ProvideServiceForTest, notifications.MockNotificationService, objectdummyserver.ProvideFakeObjectServer, diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 4d2802abbe8..45c2c8f3d12 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -143,7 +143,6 @@ import ( "github.com/grafana/grafana/pkg/services/thumbs/dashboardthumbsimpl" "github.com/grafana/grafana/pkg/services/updatechecker" "github.com/grafana/grafana/pkg/services/user/userimpl" - "github.com/grafana/grafana/pkg/services/userauth/userauthimpl" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb/azuremonitor" "github.com/grafana/grafana/pkg/tsdb/cloudmonitoring" @@ -368,7 +367,6 @@ var wireBasicSet = wire.NewSet( teamimpl.ProvideService, tempuserimpl.ProvideService, loginattemptimpl.ProvideService, - userauthimpl.ProvideService, secretsMigrations.ProvideDataSourceMigrationService, secretsMigrations.ProvideMigrateToPluginService, secretsMigrations.ProvideMigrateFromPluginService, diff --git a/pkg/services/login/authinfo.go b/pkg/services/login/authinfo.go index 3568b883c46..e8484c781ba 100644 --- a/pkg/services/login/authinfo.go +++ b/pkg/services/login/authinfo.go @@ -13,6 +13,7 @@ type AuthInfoService interface { GetExternalUserInfoByLogin(ctx context.Context, query *models.GetExternalUserInfoByLoginQuery) error SetAuthInfo(ctx context.Context, cmd *models.SetAuthInfoCommand) error UpdateAuthInfo(ctx context.Context, cmd *models.UpdateAuthInfoCommand) error + DeleteUserAuthInfo(ctx context.Context, userID int64) error } const ( diff --git a/pkg/services/login/authinfoservice/database/database.go b/pkg/services/login/authinfoservice/database/database.go index 395818e662e..a7fdf2fd65c 100644 --- a/pkg/services/login/authinfoservice/database/database.go +++ b/pkg/services/login/authinfoservice/database/database.go @@ -218,6 +218,14 @@ func (s *AuthInfoStore) DeleteAuthInfo(ctx context.Context, cmd *models.DeleteAu }) } +func (s *AuthInfoStore) DeleteUserAuthInfo(ctx context.Context, userID int64) error { + return s.sqlStore.WithDbSession(ctx, func(sess *db.Session) error { + var rawSQL = "DELETE FROM user_auth WHERE user_id = ?" + _, err := sess.Exec(rawSQL, userID) + return err + }) +} + func (s *AuthInfoStore) GetUserById(ctx context.Context, id int64) (*user.User, error) { query := user.GetUserByIDQuery{ID: id} user, err := s.userService.GetByID(ctx, &query) diff --git a/pkg/services/login/authinfoservice/service.go b/pkg/services/login/authinfoservice/service.go index ac713a7345b..3a6025bd491 100644 --- a/pkg/services/login/authinfoservice/service.go +++ b/pkg/services/login/authinfoservice/service.go @@ -197,6 +197,10 @@ func (s *Implementation) GetExternalUserInfoByLogin(ctx context.Context, query * return s.authInfoStore.GetExternalUserInfoByLogin(ctx, query) } +func (s *Implementation) DeleteUserAuthInfo(ctx context.Context, userID int64) error { + return nil +} + func (s *Implementation) Run(ctx context.Context) error { s.logger.Debug("Started AuthInfo Metrics collection service") return s.authInfoStore.RunMetricsCollection(ctx) diff --git a/pkg/services/login/logintest/logintest.go b/pkg/services/login/logintest/logintest.go index 5c2ce4005df..023ae063f13 100644 --- a/pkg/services/login/logintest/logintest.go +++ b/pkg/services/login/logintest/logintest.go @@ -57,6 +57,10 @@ func (a *AuthInfoServiceFake) GetExternalUserInfoByLogin(ctx context.Context, qu return a.ExpectedError } +func (a *AuthInfoServiceFake) DeleteUserAuthInfo(ctx context.Context, userID int64) error { + return a.ExpectedError +} + type AuthenticatorFake struct { ExpectedUser *user.User ExpectedError error diff --git a/pkg/services/userauth/userauth.go b/pkg/services/userauth/userauth.go deleted file mode 100644 index e0cb1f5c7ee..00000000000 --- a/pkg/services/userauth/userauth.go +++ /dev/null @@ -1,8 +0,0 @@ -package userauth - -import "context" - -type Service interface { - Delete(context.Context, int64) error - DeleteToken(context.Context, int64) error -} diff --git a/pkg/services/userauth/userauthimpl/store.go b/pkg/services/userauth/userauthimpl/store.go deleted file mode 100644 index e563eb396fe..00000000000 --- a/pkg/services/userauth/userauthimpl/store.go +++ /dev/null @@ -1,32 +0,0 @@ -package userauthimpl - -import ( - "context" - - "github.com/grafana/grafana/pkg/infra/db" -) - -type store interface { - Delete(context.Context, int64) error - DeleteToken(context.Context, int64) error -} - -type sqlStore struct { - db db.DB -} - -func (ss *sqlStore) Delete(ctx context.Context, userID int64) error { - return ss.db.WithDbSession(ctx, func(sess *db.Session) error { - var rawSQL = "DELETE FROM user_auth WHERE user_id = ?" - _, err := sess.Exec(rawSQL, userID) - return err - }) -} - -func (ss *sqlStore) DeleteToken(ctx context.Context, userID int64) error { - return ss.db.WithDbSession(ctx, func(sess *db.Session) error { - var rawSQL = "DELETE FROM user_auth_token WHERE user_id = ?" - _, err := sess.Exec(rawSQL, userID) - return err - }) -} diff --git a/pkg/services/userauth/userauthimpl/store_test.go b/pkg/services/userauth/userauthimpl/store_test.go deleted file mode 100644 index 5b29b735666..00000000000 --- a/pkg/services/userauth/userauthimpl/store_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package userauthimpl - -import ( - "context" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/grafana/grafana/pkg/infra/db" -) - -func TestIntegrationUserAuthDataAccess(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test") - } - - ss := db.InitTestDB(t) - userAuthStore := sqlStore{ - db: ss, - } - - t.Run("delete user auth", func(t *testing.T) { - err := userAuthStore.Delete(context.Background(), 1) - require.NoError(t, err) - }) - - t.Run("delete user auth token", func(t *testing.T) { - err := userAuthStore.DeleteToken(context.Background(), 1) - require.NoError(t, err) - }) -} diff --git a/pkg/services/userauth/userauthimpl/userauth.go b/pkg/services/userauth/userauthimpl/userauth.go deleted file mode 100644 index 23367fb9ea6..00000000000 --- a/pkg/services/userauth/userauthimpl/userauth.go +++ /dev/null @@ -1,28 +0,0 @@ -package userauthimpl - -import ( - "context" - - "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/services/userauth" -) - -type Service struct { - store store -} - -func ProvideService(db db.DB) userauth.Service { - return &Service{ - store: &sqlStore{ - db: db, - }, - } -} - -func (s *Service) Delete(ctx context.Context, userID int64) error { - return s.store.Delete(ctx, userID) -} - -func (s *Service) DeleteToken(ctx context.Context, userID int64) error { - return s.store.DeleteToken(ctx, userID) -} diff --git a/pkg/services/userauth/userauthimpl/userauth_test.go b/pkg/services/userauth/userauthimpl/userauth_test.go deleted file mode 100644 index 7c29f57d422..00000000000 --- a/pkg/services/userauth/userauthimpl/userauth_test.go +++ /dev/null @@ -1,37 +0,0 @@ -package userauthimpl - -import ( - "context" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestUserAuthService(t *testing.T) { - userAuthStore := &FakeUserAuthStore{} - userAuthService := Service{ - store: userAuthStore, - } - - t.Run("delete user", func(t *testing.T) { - err := userAuthService.Delete(context.Background(), 1) - require.NoError(t, err) - }) - - t.Run("delete token", func(t *testing.T) { - err := userAuthService.DeleteToken(context.Background(), 1) - require.NoError(t, err) - }) -} - -type FakeUserAuthStore struct { - ExpectedError error -} - -func (f *FakeUserAuthStore) Delete(ctx context.Context, userID int64) error { - return f.ExpectedError -} - -func (f *FakeUserAuthStore) DeleteToken(ctx context.Context, userID int64) error { - return f.ExpectedError -} diff --git a/pkg/services/userauth/userauthtest/fake.go b/pkg/services/userauth/userauthtest/fake.go deleted file mode 100644 index d7a3b0bb7d7..00000000000 --- a/pkg/services/userauth/userauthtest/fake.go +++ /dev/null @@ -1,19 +0,0 @@ -package userauthtest - -import "context" - -type FakeUserAuthService struct { - ExpectedError error -} - -func NewFakeUserAuthService() *FakeUserAuthService { - return &FakeUserAuthService{} -} - -func (f *FakeUserAuthService) Delete(ctx context.Context, userID int64) error { - return f.ExpectedError -} - -func (f *FakeUserAuthService) DeleteToken(ctx context.Context, userID int64) error { - return f.ExpectedError -} From 44e8fb628ed2dbb66361813b0d480941a879bf5c Mon Sep 17 00:00:00 2001 From: Giordano Ricci Date: Fri, 18 Nov 2022 14:52:54 +0100 Subject: [PATCH 013/153] Explore: Fix a11y issue with show all series button in Graph (#58943) * Explore: Fix a11y issue with show all series button in Graph * remove extra space * add spacing --- .../features/explore/Graph/ExploreGraph.tsx | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/public/app/features/explore/Graph/ExploreGraph.tsx b/public/app/features/explore/Graph/ExploreGraph.tsx index f8a0e02f0d4..ce4ec43e2c3 100644 --- a/public/app/features/explore/Graph/ExploreGraph.tsx +++ b/public/app/features/explore/Graph/ExploreGraph.tsx @@ -22,6 +22,7 @@ import { import { PanelRenderer } from '@grafana/runtime'; import { GraphDrawStyle, LegendDisplayMode, TooltipDisplayMode, SortOrder } from '@grafana/schema'; import { + Button, Icon, PanelContext, PanelContextProvider, @@ -166,13 +167,15 @@ export function ExploreGraph({ {dataWithConfig.length > MAX_NUMBER_OF_TIME_SERIES && !showAllTimeSeries && (
    - {`Showing only ${MAX_NUMBER_OF_TIME_SERIES} time series. `} - { - setShowAllTimeSeries(true); - }} - >{`Show all ${dataWithConfig.length}`} + Showing only {MAX_NUMBER_OF_TIME_SERIES} time series. +
    )} ({ timeSeriesDisclaimer: css` label: time-series-disclaimer; - width: 300px; margin: ${theme.spacing(1)} auto; padding: 10px 0; border-radius: ${theme.spacing(2)}; @@ -204,9 +206,7 @@ const getStyles = (theme: GrafanaTheme2) => ({ color: ${theme.colors.warning.main}; margin-right: ${theme.spacing(0.5)}; `, - showAllTimeSeries: css` - label: show-all-time-series; - cursor: pointer; - color: ${theme.colors.text.link}; + showAllButton: css` + margin-left: ${theme.spacing(0.5)}; `, }); From b68fe6336a89cdb17cf0eb2b97ecf590f0c9b9a5 Mon Sep 17 00:00:00 2001 From: Giordano Ricci Date: Fri, 18 Nov 2022 14:54:20 +0100 Subject: [PATCH 014/153] Chore: move keydown handler in rich history card (#58945) Chore: move kedown handler in rich history card --- public/app/features/explore/RichHistory/RichHistoryCard.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/app/features/explore/RichHistory/RichHistoryCard.tsx b/public/app/features/explore/RichHistory/RichHistoryCard.tsx index d67432a6226..882057b2928 100644 --- a/public/app/features/explore/RichHistory/RichHistoryCard.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryCard.tsx @@ -260,6 +260,7 @@ export function RichHistoryCard(props: Props) { const updateComment = (