Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5168aa63dc | |||
| 1aea178e23 |
@@ -71,6 +71,7 @@ dc_resource("tempo", labels=["observability"])
|
||||
|
||||
dc_resource("postgres", labels=["misc"])
|
||||
dc_resource("tempo-init", labels=["misc"])
|
||||
dc_resource("authelia", labels=["auth"])
|
||||
|
||||
# paths in tilt files are confusing....
|
||||
# - if tilt is dealing with the path, it is relative to the Tiltfile
|
||||
@@ -86,6 +87,7 @@ docker_build('grafana-fs-dev',
|
||||
'devenv/frontend-service/provisioning',
|
||||
'devenv/frontend-service/configs/grafana-api.local.ini',
|
||||
'devenv/frontend-service/configs/frontend-service.local.ini',
|
||||
'devenv/frontend-service/configs/authelia',
|
||||
'conf/defaults.ini',
|
||||
'public/emails',
|
||||
'public/views',
|
||||
@@ -107,6 +109,7 @@ docker_build('grafana-fs-dev',
|
||||
sync('../../public/dashboards', '/grafana/public/dashboards'),
|
||||
sync('../../public/app/plugins', '/grafana/public/app/plugins'),
|
||||
sync('../../public/build/assets-manifest.json', '/grafana/public/build/assets-manifest.json'),
|
||||
sync('./configs/authelia', '/grafana/devenv/frontend-service/configs/authelia'),
|
||||
sync('./provisioning', '/ignore/provisioning'), # Just to trigger a restart instead of rebuild
|
||||
sync('./configs/grafana-api.local.ini', '/ignore/grafana-api.local.ini'), # Just to trigger a restart instead of rebuild
|
||||
sync('./configs/frontend-service.local.ini', '/ignore/frontend-service.local.ini'), # Just to trigger a restart instead of rebuild
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
---
|
||||
theme: light
|
||||
|
||||
server:
|
||||
address: 'tcp://:9091/'
|
||||
|
||||
log:
|
||||
level: debug
|
||||
format: text
|
||||
|
||||
webauthn:
|
||||
disable: true
|
||||
|
||||
duo_api:
|
||||
disable: true
|
||||
|
||||
authentication_backend:
|
||||
file:
|
||||
path: /config/users_database.yml
|
||||
password:
|
||||
algorithm: 'argon2'
|
||||
password_change:
|
||||
disable: true
|
||||
password_reset:
|
||||
disable: true
|
||||
|
||||
session:
|
||||
secret: insecure_session_secret
|
||||
cookies:
|
||||
- name: authelia_session
|
||||
domain: oauth.localhost
|
||||
authelia_url: https://oauth.localhost:9091
|
||||
expiration: '1 hour'
|
||||
inactivity: '5 minutes'
|
||||
remember_me: '1 month'
|
||||
|
||||
access_control:
|
||||
default_policy: one_factor
|
||||
|
||||
storage:
|
||||
encryption_key: you_must_generate_a_secret_of_more_than_twenty_chars_and_configure_this
|
||||
local:
|
||||
path: /config/db.sqlite3
|
||||
|
||||
notifier:
|
||||
filesystem:
|
||||
filename: /config/notification.txt
|
||||
|
||||
identity_validation:
|
||||
reset_password:
|
||||
jwt_secret: a_very_important_secret
|
||||
|
||||
identity_providers:
|
||||
oidc:
|
||||
hmac_secret: this_is_a_secret_abc123abc123abc123
|
||||
jwks:
|
||||
- algorithm: RS256
|
||||
key: |
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCbKAl8ITAb+1I8
|
||||
61R1bp2ZbZuSuKxUYXuCGWdGa+lhn45sVJz2HkqtYe7m/BG87WbhiJv5LYvMApFW
|
||||
ajm95LLqp8z3zPzD0ULy3Nks2yNIuMSVyT7/AltvO5wg11gRJZjSkre7knRF/KP6
|
||||
ARbCwF/d1iLwXIWeGj+hCzaz3CPfMm7kSVzXUkzC1XsU38fb9TbDVHm1gQMluiKo
|
||||
Yrmsz2DX6nyTSqJXfj6iblcjePs8Y9jzLNbyda7NyxA6k8WhKMwIHdCQxajsJwy6
|
||||
AnkYLTLvNlR+kD6xa4hSCUs/eDo8zqFWybEchvwwbp/Xys443GgDUf/iGbZbBl09
|
||||
tYWajIerAgMBAAECggEABzAHoHR5IhK2cJQGSZpiOqVVO6rKcO5DJne+zQhau2cE
|
||||
1gflbZFhrD6JLrImsDXfOjt13kk53K2RxgsNubpKf4xedmxMmVWsYEvS070jGU9V
|
||||
7ApynKWjTrfYN71CGyk+tyKM2Gekc0mM3xwFzefTiRdObwwaNITKxc8bLaHZF5HP
|
||||
/FMG2zqf84TU1six4JamVxMK3EQSzBc1PnYoSa5W8hdQCQkZTx+T4YoGSi7UIZG1
|
||||
BAxQN8u9Dj2la8fwd8ttT+ErGScyQqbfgCbs9/5guZSHXq/qBrMJ4LP/Ir8+VVfo
|
||||
lrMtxotHWKvOdZqSy0D9GlUDo7i/tgShrUGojCnQwQKBgQDbZ493k6DvcIlu8N5d
|
||||
kMMScX+zi6jTQtBfJXhDRybj4i+nUTWlRbnhNAZhD7gl+z+ngnnTWqmULTAPm2Np
|
||||
jZudARUNiR+iRJonLrVMWeHF2nrE5/wrF3NDe7gyUxE2lAUy/wFFbAIxCp9pD50s
|
||||
bafU+OYTGe9tTzjo+40PbgMocwKBgQC1CR7QKZjP0WNKO9LQYhY8y8/jkM58GWJm
|
||||
R5qhlVTJ0yLgBZy3kdxlbpUsBoE6aByOC70cNKOO0vh1iimXzGSdtznBJc+PxiXX
|
||||
zDFsPoS0QHlfbCQJmYrwiZhHPNAoCWYbJo8KvkIwVnM1UFrLdpCfk9sQIoZph46+
|
||||
o7CSsI2t6QKBgBjA7FzPWR7qkXbk0hG4XWndSE0XeqqrJRs2/QSKKIcZY8r6zJSi
|
||||
8z/HQNj+jwYp/JqHi/sehXdkScHZBDRKd74U+y0VxW3nU4UMLgQ5N9G8vpEsozSx
|
||||
Zp28faGf4ZdIx/Vi89/DOdQSoL2Xt2Hl1UOf+UU6bdrlT0Rp7RZKkSylAoGAfBWc
|
||||
QSHB0++5FpCalqokg9dOzrPaU8UyZNh/bHFmhE9rgBFYsZoQbpW1OU/cE6R4rgPt
|
||||
wv9xe9uu4SGqEJnP/SoxM+ousmUmWxtiZMcVPldS2czNhqbvTJ+C+JD+O/L1QXbU
|
||||
ZJCz3V3j6Y8CLKM/zaESbaS8bGi1toWL6X+KHMECgYEAloauyKHsgtKiNF3D8OmI
|
||||
unN2E9NnLGFg7R59C0j+T9+qzi5Mkfeqef5euOa7KwkbKmhr1SmGW4sjtJ2m4sdD
|
||||
pyTE9OuMKK7O8xXNSRLEoBk8gjodbeobYPFPjOkAJRZZFnP2JuLhyEklP1P4J0WZ
|
||||
pPBw/R+JfWyZAqJcwrNdmgU=
|
||||
-----END PRIVATE KEY-----
|
||||
lifespans:
|
||||
access_token: '1 hour'
|
||||
authorize_code: '1 minute'
|
||||
id_token: '1 hour'
|
||||
refresh_token: '90 minutes'
|
||||
enable_client_debug_messages: true
|
||||
authorization_policies:
|
||||
grafana_group_policy:
|
||||
default_policy: 'deny'
|
||||
rules:
|
||||
- policy: 'one_factor'
|
||||
subject: 'group:grafana'
|
||||
clients:
|
||||
- client_id: 'grafana'
|
||||
consent_mode: implicit
|
||||
client_name: 'Grafana'
|
||||
client_secret: '$pbkdf2-sha512$310000$c8p78n7pUMln0jzvd4aK4Q$JNRBzwAo0ek5qKn50cFzzvE9RXV88h1wJn5KGiHrD0YKtZaR/nCb2CJPOsKaPK0hjf.9yHxzQGZziziccp6Yng' # The digest of 'insecure_secret'.
|
||||
public: false
|
||||
authorization_policy: 'grafana_group_policy'
|
||||
require_pkce: true
|
||||
pkce_challenge_method: 'S256'
|
||||
redirect_uris:
|
||||
- http://localhost:3000/login/generic_oauth
|
||||
scopes:
|
||||
- 'openid'
|
||||
- 'profile'
|
||||
- 'groups'
|
||||
- 'email'
|
||||
response_types:
|
||||
- 'code'
|
||||
grant_types:
|
||||
- 'authorization_code'
|
||||
access_token_signed_response_alg: 'RS256'
|
||||
userinfo_signed_response_alg: 'none'
|
||||
token_endpoint_auth_method: 'client_secret_basic'
|
||||
@@ -0,0 +1,45 @@
|
||||
# This file defines test users for local development.
|
||||
# Password: all users have password "password"
|
||||
# Password hash generated with: docker run --rm authelia/authelia:latest authelia crypto hash generate argon2 --password 'password'
|
||||
#
|
||||
# Users must in the 'grafana' group to be able to log in to Grafana.
|
||||
|
||||
users:
|
||||
# Admin user with GrafanaAdmin role
|
||||
oauth-admin:
|
||||
disabled: false
|
||||
displayname: 'Admin User'
|
||||
password: '$argon2id$v=19$m=65536,t=3,p=4$an3cCNeaYsC2sFIc1LrCLQ$YxvcYkj2NTUsV5o5wxeIc0KNhqc8/mPIKKC6e1AnC4A'
|
||||
email: test1@example.com
|
||||
groups:
|
||||
- grafana
|
||||
- grafanaadmin
|
||||
|
||||
# Editor user
|
||||
oauth-editor:
|
||||
disabled: false
|
||||
displayname: 'Editor User'
|
||||
password: '$argon2id$v=19$m=65536,t=3,p=4$an3cCNeaYsC2sFIc1LrCLQ$YxvcYkj2NTUsV5o5wxeIc0KNhqc8/mPIKKC6e1AnC4A'
|
||||
email: test2@example.com
|
||||
groups:
|
||||
- grafana
|
||||
- editor
|
||||
|
||||
# Viewer user
|
||||
oauth-viewer:
|
||||
disabled: false
|
||||
displayname: 'Viewer User'
|
||||
password: '$argon2id$v=19$m=65536,t=3,p=4$an3cCNeaYsC2sFIc1LrCLQ$YxvcYkj2NTUsV5o5wxeIc0KNhqc8/mPIKKC6e1AnC4A'
|
||||
email: test3@example.com
|
||||
groups:
|
||||
- grafana
|
||||
- viewer
|
||||
|
||||
# Denied access user (not in grafana group)
|
||||
norole:
|
||||
disabled: false
|
||||
displayname: 'No Role User'
|
||||
password: '$argon2id$v=19$m=65536,t=3,p=4$an3cCNeaYsC2sFIc1LrCLQ$YxvcYkj2NTUsV5o5wxeIc0KNhqc8/mPIKKC6e1AnC4A'
|
||||
email: norole@example.com
|
||||
groups:
|
||||
- users
|
||||
@@ -73,7 +73,7 @@ server {
|
||||
}
|
||||
|
||||
# API calls go to the backend
|
||||
location ~ ^/(api|apis|avatar|bootdata|render|logout|public/plugins|public/openapi3|public/api-merged|goto|swagger|openapi/v3) {
|
||||
location ~ ^/(api|apis|avatar|bootdata|render|logout|public/plugins|public/openapi3|public/api-merged|goto|swagger|openapi/v3|login/generic_oauth) {
|
||||
# Add debug headers to the response
|
||||
add_header Nginx-Trace-Id $otel_trace_id always;
|
||||
add_header Nginx-Route "backend" always;
|
||||
|
||||
@@ -41,7 +41,7 @@ services:
|
||||
GF_FEATURE_TOGGLES_ENABLE: enableNativeHTTPHistogram
|
||||
GF_DATABASE_URL: postgres://grafana:grafana@postgres:5432/grafana
|
||||
GF_SERVER_ROUTER_LOGGING: true
|
||||
GF_LOG_LEVEL: info
|
||||
GF_LOG_LEVEL: debug
|
||||
GF_AUTH_LOGIN_COOKIE_NAME: grafana_fs_dev_login # set a custom cookie name to not conflict with other instances running on localhost
|
||||
OTEL_SERVICE_NAME: grafana-api
|
||||
GF_TRACING_OPENTELEMETRY_OTLP_ADDRESS: 'alloy:4317'
|
||||
@@ -150,6 +150,62 @@ services:
|
||||
tempo-init:
|
||||
condition: service_completed_successfully
|
||||
|
||||
<<<<<<< ours
|
||||
authelia:
|
||||
image: authelia/authelia:latest
|
||||
container_name: authelia
|
||||
volumes:
|
||||
- ./configs/authelia/configuration.yml:/config/configuration.yml
|
||||
- ./configs/authelia/users_database.yml:/config/users_database.yml
|
||||
ports:
|
||||
- '9091:9091'
|
||||
environment:
|
||||
- TZ=UTC
|
||||
labels:
|
||||
- 'alloy.logs=true'
|
||||
|
||||
authelia:
|
||||
image: authelia/authelia:latest
|
||||
container_name: authelia
|
||||
volumes:
|
||||
- ./configs/authelia/configuration.yml:/config/configuration.yml
|
||||
- ./configs/authelia/users_database.yml:/config/users_database.yml
|
||||
ports:
|
||||
- '9091:9091'
|
||||
environment:
|
||||
- TZ=UTC
|
||||
labels:
|
||||
- 'alloy.logs=true'
|
||||
|
||||
authelia:
|
||||
image: authelia/authelia:latest
|
||||
container_name: authelia
|
||||
volumes:
|
||||
- ./configs/authelia/configuration.yml:/config/configuration.yml
|
||||
- ./configs/authelia/users_database.yml:/config/users_database.yml
|
||||
ports:
|
||||
- '9091:9091'
|
||||
environment:
|
||||
- TZ=UTC
|
||||
labels:
|
||||
- 'alloy.logs=true'
|
||||
|
||||
||||||| ancestor
|
||||
=======
|
||||
authelia:
|
||||
image: authelia/authelia:latest
|
||||
container_name: authelia
|
||||
volumes:
|
||||
- ./configs/authelia/configuration.yml:/config/configuration.yml
|
||||
- ./configs/authelia/users_database.yml:/config/users_database.yml
|
||||
ports:
|
||||
- '9091:9091'
|
||||
environment:
|
||||
- TZ=UTC
|
||||
labels:
|
||||
- 'alloy.logs=true'
|
||||
|
||||
>>>>>>> theirs
|
||||
volumes:
|
||||
backend-data:
|
||||
postgres-data:
|
||||
|
||||
@@ -187,6 +187,112 @@ func TestFrontendService_Middleware(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestFrontendService_LoginErrorCookie(t *testing.T) {
|
||||
publicDir := setupTestWebAssets(t)
|
||||
cfg := &setting.Cfg{
|
||||
HTTPPort: "3000",
|
||||
StaticRootPath: publicDir,
|
||||
BuildVersion: "10.3.0",
|
||||
OAuthLoginErrorMessage: "oauth.login.error",
|
||||
CookieSecure: false,
|
||||
CookieSameSiteDisabled: false,
|
||||
CookieSameSiteMode: http.SameSiteLaxMode,
|
||||
}
|
||||
|
||||
t.Run("should detect login_error cookie and set generic error message", func(t *testing.T) {
|
||||
service := createTestService(t, cfg)
|
||||
|
||||
mux := web.New()
|
||||
service.addMiddlewares(mux)
|
||||
service.registerRoutes(mux)
|
||||
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
// Set the login_error cookie (with some encrypted-looking value)
|
||||
req.AddCookie(&http.Cookie{
|
||||
Name: "login_error",
|
||||
Value: "abc123encryptedvalue",
|
||||
})
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
mux.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
body := recorder.Body.String()
|
||||
|
||||
// Check that the generic error message is in the response
|
||||
assert.Contains(t, body, "loginError", "Should contain loginError when cookie is present")
|
||||
assert.Contains(t, body, "oauth.login.error", "Should contain the generic OAuth error message")
|
||||
|
||||
// Check that the cookie was deleted (MaxAge=-1)
|
||||
cookies := recorder.Result().Cookies()
|
||||
var foundDeletedCookie bool
|
||||
for _, cookie := range cookies {
|
||||
if cookie.Name == "login_error" {
|
||||
assert.Equal(t, -1, cookie.MaxAge, "Cookie should be deleted (MaxAge=-1)")
|
||||
assert.Equal(t, "", cookie.Value, "Cookie value should be empty")
|
||||
foundDeletedCookie = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, foundDeletedCookie, "Should have set a cookie deletion header")
|
||||
})
|
||||
|
||||
t.Run("should not set error when login_error cookie is absent", func(t *testing.T) {
|
||||
service := createTestService(t, cfg)
|
||||
|
||||
mux := web.New()
|
||||
service.addMiddlewares(mux)
|
||||
service.registerRoutes(mux)
|
||||
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
// No login_error cookie
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
mux.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
body := recorder.Body.String()
|
||||
|
||||
// The page should render but without the login error
|
||||
assert.Contains(t, body, "window.grafanaBootData")
|
||||
// Check that loginError is not set (or is empty/omitted in JSON)
|
||||
// Since it's omitempty, it shouldn't appear at all
|
||||
assert.NotContains(t, body, "loginError", "Should not contain loginError when cookie is absent")
|
||||
})
|
||||
|
||||
t.Run("should handle custom OAuth error message from config", func(t *testing.T) {
|
||||
customCfg := &setting.Cfg{
|
||||
HTTPPort: "3000",
|
||||
StaticRootPath: publicDir,
|
||||
BuildVersion: "10.3.0",
|
||||
OAuthLoginErrorMessage: "Oh no a boo-boo happened!",
|
||||
CookieSecure: false,
|
||||
CookieSameSiteDisabled: false,
|
||||
CookieSameSiteMode: http.SameSiteLaxMode,
|
||||
}
|
||||
service := createTestService(t, customCfg)
|
||||
|
||||
mux := web.New()
|
||||
service.addMiddlewares(mux)
|
||||
service.registerRoutes(mux)
|
||||
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
req.AddCookie(&http.Cookie{
|
||||
Name: "login_error",
|
||||
Value: "abc123encryptedvalue",
|
||||
})
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
mux.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
body := recorder.Body.String()
|
||||
|
||||
// Check that the custom error message is used
|
||||
assert.Contains(t, body, "Oh no a boo-boo happened!", "Should use custom OAuth error message from config")
|
||||
})
|
||||
}
|
||||
|
||||
func TestFrontendService_IndexHooks(t *testing.T) {
|
||||
publicDir := setupTestWebAssets(t)
|
||||
cfg := &setting.Cfg{
|
||||
|
||||
@@ -47,4 +47,6 @@ type FSFrontendSettings struct {
|
||||
CSPReportOnlyEnabled bool `json:"cspReportOnlyEnabled,omitempty"`
|
||||
Http2Enabled bool `json:"http2Enabled,omitempty"`
|
||||
ReportingStaticContext map[string]string `json:"reportingStaticContext,omitempty"`
|
||||
|
||||
LoginError string `json:"loginError,omitempty"`
|
||||
}
|
||||
|
||||
@@ -148,6 +148,30 @@ func (p *IndexProvider) HandleRequest(writer http.ResponseWriter, request *http.
|
||||
data.Nonce = nonce
|
||||
data.PublicDashboardAccessToken = reqCtx.PublicDashboardAccessToken
|
||||
|
||||
// TODO -- reevaluate with mt authnz
|
||||
// Check for login_error cookie and set a generic error message.
|
||||
// The backend sets an encrypted cookie on oauth login failures that we can't read
|
||||
// so we just show a generic error if the cookie is present.
|
||||
if cookie, err := request.Cookie("login_error"); err == nil && cookie.Value != "" {
|
||||
p.log.Info("request has login_error cookie")
|
||||
// Defaults to a translation key that the frontend will resolve to a localized message
|
||||
data.Settings.LoginError = p.data.Config.OAuthLoginErrorMessage
|
||||
|
||||
cookiePath := "/"
|
||||
if p.data.AppSubUrl != "" {
|
||||
cookiePath = p.data.AppSubUrl
|
||||
}
|
||||
http.SetCookie(writer, &http.Cookie{
|
||||
Name: "login_error",
|
||||
Value: "",
|
||||
Path: cookiePath,
|
||||
MaxAge: -1,
|
||||
HttpOnly: true,
|
||||
Secure: p.data.Config.CookieSecure,
|
||||
SameSite: p.data.Config.CookieSameSiteMode,
|
||||
})
|
||||
}
|
||||
|
||||
if data.CSPEnabled {
|
||||
data.CSPContent = middleware.ReplacePolicyVariables(p.data.CSPContent, p.data.AppSubUrl, data.Nonce)
|
||||
writer.Header().Set("Content-Security-Policy", data.CSPContent)
|
||||
|
||||
Reference in New Issue
Block a user