Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[!] implement webui authentication, closes #216 #251

Merged
merged 96 commits into from
Aug 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
96 commits
Select commit Hold shift + click to select a range
115952c
layout for authentication created
denys-holub Jun 22, 2023
be30e17
routes for setting password & logging created
denys-holub Jun 22, 2023
2929eb1
unused import deleted
denys-holub Jun 22, 2023
537b456
public and private routes created
denys-holub Jul 4, 2023
fd353f0
public and private routes implemented
denys-holub Jul 4, 2023
c69878c
session storage services created
denys-holub Jul 4, 2023
1ea8ce8
null type added
denys-holub Jul 4, 2023
b281b39
private routes wrapper created
denys-holub Jul 4, 2023
9eab0a0
logout icon with imitating logout function added
denys-holub Jul 4, 2023
732ea95
one layout for signup and signin deleted
denys-holub Jul 4, 2023
8ff63be
layout for signin created
denys-holub Jul 4, 2023
731498e
layout for signup created
denys-holub Jul 4, 2023
7f5058f
[!] implement new Dockerfile based on official Postgres image, closes…
pashagolub Jun 28, 2023
40200a4
[*] update supported PostgreSQL versions in `README.md`
pashagolub Jun 30, 2023
9e7fb6f
Bump github.com/hashicorp/consul/api from 1.21.0 to 1.22.0 in /src
dependabot[bot] Jul 3, 2023
ca608ec
Bump github.com/shirou/gopsutil/v3 from 3.23.5 to 3.23.6 in /src
dependabot[bot] Jul 3, 2023
06823a9
Bump golang.org/x/crypto from 0.10.0 to 0.11.0 in /src
dependabot[bot] Jul 6, 2023
cf18e9b
Bump tough-cookie from 4.1.2 to 4.1.3 in /src/webui
dependabot[bot] Jul 10, 2023
2bfcf3d
[!] bump Grafana to v10, closes #227
pashagolub Jul 10, 2023
e0bc7a5
Bump github.com/jackc/pgx/v5 from 5.4.1 to 5.4.2 in /src
dependabot[bot] Jul 12, 2023
819b23c
Bump semver from 6.3.0 to 6.3.1 in /src/webui
dependabot[bot] Jul 12, 2023
b0b1f87
sign up page deleted
denys-holub Jul 12, 2023
dbe471c
form added, request implemented
denys-holub Jul 12, 2023
30e86d5
sign up route deleted
denys-holub Jul 12, 2023
f4c15fb
auth form type created
denys-holub Jul 12, 2023
316817b
auth service created, login request created
denys-holub Jul 12, 2023
2aaa0d0
login request implemented
denys-holub Jul 12, 2023
08a5a6b
comments removed
denys-holub Jul 12, 2023
66b8e25
[+] add JWT handling to the back-end
pashagolub Jul 12, 2023
72a7ee8
header 'Token' added to request
denys-holub Jul 13, 2023
34fb9d8
given param changed
denys-holub Jul 13, 2023
f8e767a
error alert added
denys-holub Jul 13, 2023
5ddaf9b
common alert component created
denys-holub Jul 13, 2023
b3bfbf1
token expiration time validation changed
denys-holub Jul 13, 2023
8311510
merge master
pashagolub Jul 13, 2023
0dbdcad
simplify password check
pashagolub Jul 13, 2023
c6f4ce9
login form height & width changed
denys-holub Jul 17, 2023
0e851b2
logout function created
denys-holub Jul 17, 2023
59e1f64
logout function implemented
denys-holub Jul 17, 2023
7a31e8b
axios instance with request interceptor created
denys-holub Jul 17, 2023
d9a67a0
axios instance implemented
denys-holub Jul 17, 2023
577ffe6
alert context & provider created
denys-holub Jul 19, 2023
5b575c0
alert context provider implemented
denys-holub Jul 19, 2023
2500a11
compiled warnings fixed
denys-holub Jul 19, 2023
0d567c6
onSuccess & onError actions created
denys-holub Jul 19, 2023
b6f8024
compiled warnings fixed
denys-holub Jul 19, 2023
f612f80
compiled warnings fixed
denys-holub Jul 19, 2023
7983e40
alert context used
denys-holub Jul 19, 2023
a0d4265
alert context used
denys-holub Jul 19, 2023
f87ee67
styles added
denys-holub Jul 20, 2023
d2ab702
query & mutation 'onError' interceptors created
denys-holub Jul 20, 2023
6dafcb0
'isUnauthorized' method created
denys-holub Jul 20, 2023
756b48c
method name changed
denys-holub Jul 20, 2023
ecc1542
clickaway & escapeKeyDown reasons for alert close avoided
denys-holub Jul 21, 2023
fbcc22c
auto hide duration set
denys-holub Jul 21, 2023
99e96d5
Dbs related queries & mutations placed in a seperate file
denys-holub Jul 21, 2023
6702767
queries & mutations implemented
denys-holub Jul 21, 2023
5bd09f4
mutations implemented
denys-holub Jul 21, 2023
9a3016f
mutation directly implemented
denys-holub Jul 21, 2023
e57f95d
enable db monitoring mutation from function params removed
denys-holub Jul 21, 2023
a4e5a82
global alert implemented
denys-holub Jul 21, 2023
7f8102a
unique metrics key added
denys-holub Jul 21, 2023
86788e1
metric related queries & mutations placed in separate file
denys-holub Jul 21, 2023
f3741e2
global alert implemented
denys-holub Jul 21, 2023
83cdd41
mutations implemented
denys-holub Jul 21, 2023
b58cde3
use unique metrics query removed
denys-holub Jul 24, 2023
c860bb5
preset related queries & mutations placed in a separate file
denys-holub Jul 24, 2023
113aeeb
query & mutation implemented
denys-holub Jul 24, 2023
db21578
mutations implemented
denys-holub Jul 24, 2023
46119d9
default use metrics query implemented
denys-holub Jul 24, 2023
4f7c0bf
onError action added
denys-holub Jul 24, 2023
9f068bc
alert call & navigate implemented
denys-holub Jul 24, 2023
a6a9fab
compiled warnings fixed
denys-holub Jul 24, 2023
8c7e7f7
allow public connection
pashagolub Jul 31, 2023
18d8bae
Merge branch '216-implement-webui-authentication' of https://github.c…
pashagolub Jul 31, 2023
c60fdbe
query client provider created, onSuccess & onError actions added
denys-holub Jul 31, 2023
82dc642
default onSuccess & onError actions removed
denys-holub Jul 31, 2023
757eaa5
unused params deleted
denys-holub Jul 31, 2023
055c695
compiled warnings fixed
denys-holub Jul 31, 2023
1da28ed
@tanstack/react-query upgraded
denys-holub Aug 3, 2023
4b9ef21
required rules removed
denys-holub Aug 3, 2023
cb56c34
new request for default login created
denys-holub Aug 7, 2023
24c9045
new request for default login implemented
denys-holub Aug 7, 2023
c153ec2
useLoginDefault hook implemented
denys-holub Aug 7, 2023
67b704a
comments removed
denys-holub Aug 7, 2023
085fba8
logout action added
denys-holub Aug 7, 2023
cb5bc25
merge branch 'master' into 216-implement-webui-authentication
pashagolub Aug 7, 2023
c6305dd
go mod tidy
pashagolub Aug 7, 2023
5fe4204
comments deleted
denys-holub Aug 8, 2023
b3468a5
default login request removed
denys-holub Aug 8, 2023
d6b2aaa
file extension changed
denys-holub Aug 8, 2023
2d592bf
component import changed
denys-holub Aug 8, 2023
5855489
option replace to navigate action added
denys-holub Aug 8, 2023
6ceb7f1
default login request removed
denys-holub Aug 8, 2023
282d9d9
add default values for WebUI user and password
pashagolub Aug 8, 2023
b52863d
disallow empty login credentials
pashagolub Aug 8, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file modified build-docker-demo.sh
100644 → 100755
Empty file.
9 changes: 9 additions & 0 deletions docker/bootstrap/1_create_role_db.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
CREATE ROLE pgwatch3 WITH
IN ROLE pg_monitor
LOGIN PASSWORD 'pgwatch3admin'; -- change the pw for production

CREATE DATABASE pgwatch3 OWNER pgwatch3;

CREATE DATABASE pgwatch3_grafana OWNER pgwatch3;

CREATE DATABASE pgwatch3_metrics OWNER pgwatch3;
26 changes: 26 additions & 0 deletions docker/bootstrap/2_init_pgwatch_db.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/bin/bash

psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "pgwatch3" <<-EOSQL
CREATE EXTENSION pg_qualstats;
CREATE EXTENSION plpython3u;
EOSQL

psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "pgwatch3" \
-f /pgwatch3/metrics/00_helpers/get_load_average/9.1/metric.sql \
-f /pgwatch3/metrics/00_helpers/get_stat_statements/9.4/metric.sql \
-f /pgwatch3/metrics/00_helpers/get_stat_activity/9.2/metric.sql \
-f /pgwatch3/metrics/00_helpers/get_stat_replication/9.2/metric.sql \
-f /pgwatch3/metrics/00_helpers/get_table_bloat_approx/9.5/metric.sql \
-f /pgwatch3/metrics/00_helpers/get_table_bloat_approx_sql/12/metric.sql \
-f /pgwatch3/metrics/00_helpers/get_wal_size/10/metric.sql \
-f /pgwatch3/metrics/00_helpers/get_psutil_cpu/9.1/metric.sql \
-f /pgwatch3/metrics/00_helpers/get_psutil_mem/9.1/metric.sql \
-f /pgwatch3/metrics/00_helpers/get_psutil_disk/9.1/metric.sql \
-f /pgwatch3/metrics/00_helpers/get_psutil_disk_io_total/9.1/metric.sql


if [ "$PW3_PG_SCHEMA_TYPE" == "timescale" ] ; then
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "pgwatch3_metrics" <<-EOSQL
CREATE EXTENSION timescaledb;
EOSQL
fi
38 changes: 38 additions & 0 deletions docker/daemon/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# ----------------------------------------------------------------
# 1. Build Web UI
# ----------------------------------------------------------------
FROM node:19 AS uibuilder
ADD src/webui /webui
RUN cd webui && yarn install --network-timeout 100000 && yarn build

# ----------------------------------------------------------------
# 2. Build gatherer
# ----------------------------------------------------------------
FROM golang:1.20 as builder
ARG GIT_HASH
ARG GIT_TIME
ENV GIT_HASH=${GIT_HASH}
ENV GIT_TIME=${GIT_TIME}

ADD src /pgwatch3
COPY --from=uibuilder /webui/build /pgwatch3/webui/build
RUN cd /pgwatch3 && bash build_gatherer.sh

# ----------------------------------------------------------------
# 3. Build the final image
# ----------------------------------------------------------------
FROM alpine

# Copy over the compiled gatherer
COPY --from=builder /pgwatch3/pgwatch3 /pgwatch3/
COPY src/metrics /pgwatch3/metrics

# Admin UI for configuring servers to be monitored
EXPOSE 8080
# Gatherer healthcheck port / metric statistics (JSON)
EXPOSE 8081
# Prometheus metrics scraping port
EXPOSE 9187

# Command to run the executable
ENTRYPOINT ["/pgwatch3/pgwatch3"]
3 changes: 3 additions & 0 deletions docker/demo/grafana.ini
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@ org_name = Main Org.
# Role for unauthenticated users, other valid values are `Editor` and `Admin`
org_role = Editor

<<<<<<<< HEAD:grafana/grafana_custom_config.ini
========
[dashboards]
default_home_dashboard_path = /var/lib/grafana/dashboards/1-global-db-overview.json
>>>>>>>> master:docker/demo/grafana.ini

[metrics]
enabled = false
21 changes: 20 additions & 1 deletion docker/demo/supervisord.conf
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ stdout_logfile_maxbytes=0

[program:postgres]
command=/usr/local/bin/docker-entrypoint.sh postgres -c config_file=/etc/postgresql/postgresql.conf
<<<<<<<< HEAD:docker/supervisord.conf
startsecs=0
========
startsecs=5
>>>>>>>> master:docker/demo/supervisord.conf
priority=100
stopsignal=INT
autostart=false
Expand All @@ -48,5 +52,20 @@ user=grafana
startsecs=5
priority=500
autostart=false
<<<<<<<< HEAD:docker/supervisord.conf
autorestart=false
redirect_stderr=true

[program:grafana_dashboard_setup]
command=/pgwatch3/bootstrap/set_up_grafana_dashboards.sh
priority=600
autorestart=false
startsecs=0
autostart=false
redirect_stderr=true
stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0
========
autorestart=true
redirect_stderr=true
redirect_stderr=true
>>>>>>>> master:docker/demo/supervisord.conf
4 changes: 2 additions & 2 deletions src/config/cmdparser.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ type StartOpts struct {
// WebUIOpts specifies the internal web UI server options
type WebUIOpts struct {
WebAddr string `long:"web-addr" mapstructure:"web-addr" description:"TCP address in the form 'host:port' to listen on" default:":8080" env:"PW3_WEBADDR"`
WebUser string `long:"web-user" mapstructure:"web-user" description:"Admin login" env:"PW3_WEBUSER"`
WebPassword string `long:"web-password" mapstructure:"web-password" description:"Admin password" env:"PW3_WEBPASSWORD"`
WebUser string `long:"web-user" mapstructure:"web-user" description:"Admin login" env:"PW3_WEBUSER" default:"pgwatch3"`
WebPassword string `long:"web-password" mapstructure:"web-password" description:"Admin password" env:"PW3_WEBPASSWORD" default:"pgwatch3admin"`
}

type CmdOptions struct {
Expand Down
1 change: 1 addition & 0 deletions src/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.20

require (
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/gorilla/websocket v1.5.0
github.com/hashicorp/consul/api v1.24.0
github.com/jackc/pgx/v5 v5.4.3
Expand Down
2 changes: 2 additions & 0 deletions src/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5x
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
Expand Down
108 changes: 108 additions & 0 deletions src/webserver/jwt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package webserver

import (
"encoding/json"
"errors"
"fmt"
"net/http"
"time"

"github.com/golang-jwt/jwt"
)

type loginReq struct {
Username string `json:"user"`
Password string `json:"password"`
}

func (Server *WebUIServer) IsCorrectPassword(lr loginReq) bool {
return Server.WebUser == lr.Username && Server.WebPassword == lr.Password
}

func (Server *WebUIServer) handleLogin(w http.ResponseWriter, r *http.Request) {
var (
err error
lr loginReq
token string
)

defer func() {
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}()

switch r.Method {
case "POST":
if err = json.NewDecoder(r.Body).Decode(&lr); err != nil {
return
}
if !Server.IsCorrectPassword(lr) {
http.Error(w, "can not authenticate this user", http.StatusUnauthorized)
return
}
if token, err = generateJWT(lr.Username); err != nil {
return
}
_, err = w.Write([]byte(token))

case "GET":
fmt.Fprintf(w, "only POST methods is allowed.")
return
}
}

type EnsureAuth struct {
handler http.HandlerFunc
}

func (ea *EnsureAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err := validateToken(r); err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
ea.handler(w, r)
}

func NewEnsureAuth(handlerToWrap http.HandlerFunc) *EnsureAuth {
return &EnsureAuth{handlerToWrap}
}

var sampleSecretKey = []byte("5m3R7K4754p4m")

func generateJWT(username string) (string, error) {
token := jwt.New(jwt.SigningMethodHS256)
claims := token.Claims.(jwt.MapClaims)

claims["authorized"] = true
claims["username"] = username
claims["exp"] = time.Now().Add(time.Hour * 8).Unix()

return token.SignedString(sampleSecretKey)
}

func validateToken(r *http.Request) (err error) {
if r.Header["Token"] == nil {
return errors.New("can not find token in header")
}
token, err := jwt.Parse(r.Header["Token"][0], func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.New("there was an error in parsing")
}
return sampleSecretKey, nil
})
if err != nil {
return err
}
if token == nil {
return errors.New("invalid token")
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return errors.New("cannot parse token claims")
}
if !claims.VerifyExpiresAt(time.Now().Local().Unix(), true) {
return errors.New("token expired")
}
return nil
}
11 changes: 6 additions & 5 deletions src/webserver/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,12 @@ func Init(opts config.WebUIOpts, webuifs fs.FS, api apiHandler, logger log.Logge
api,
}

mux.HandleFunc("/db", s.handleDBs)
mux.HandleFunc("/metric", s.handleMetrics)
mux.HandleFunc("/preset", s.handlePresets)
mux.HandleFunc("/stats", s.handleStats)
mux.HandleFunc("/log", s.serveWsLog)
mux.Handle("/db", NewEnsureAuth(s.handleDBs))
mux.Handle("/metric", NewEnsureAuth(s.handleMetrics))
mux.Handle("/preset", NewEnsureAuth(s.handlePresets))
mux.Handle("/stats", NewEnsureAuth(s.handleStats))
mux.Handle("/log", NewEnsureAuth(s.serveWsLog))
mux.HandleFunc("/login", s.handleLogin)
mux.HandleFunc("/", s.handleStatic)

go func() { panic(s.ListenAndServe()) }()
Expand Down
30 changes: 22 additions & 8 deletions src/webui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,37 @@ import { Box, Toolbar } from "@mui/material";
import CssBaseline from "@mui/material/CssBaseline";
import { ThemeProvider, createTheme } from "@mui/material/styles";

import { QueryClientProvider } from "@tanstack/react-query";
import { queryClient } from "queryClient";
import { QueryClientProvider } from "QueryClient";

import { Route, Routes } from "react-router-dom";
import { PrivateRoute } from "layout/PrivateRoute";
import { privateRoutes, publicRoutes } from "layout/Routes";

import { AlertComponent } from "layout/common/AlertComponent";
import { AppBar } from "./layout/AppBar";
import { routes } from "./layout/Routes";

const mdTheme = createTheme();

export default function App() {
const routesItems = useMemo(

const publicRoutesItems = useMemo(
() =>
publicRoutes.map((route) => (
<Route key={route.link} path={route.link} element={<route.element />} />
)),
[]
);

const privateRoutesItems = useMemo(
() =>
routes.map((route) => (
<Route key={route.link} path={route.link} element={route.element()} />
privateRoutes.map((route) => (
<Route key={route.link} path={route.link} element={<PrivateRoute><route.element /></PrivateRoute>} />
)),
[]
);

return (
<QueryClientProvider client={queryClient}>
<QueryClientProvider>
<ThemeProvider theme={mdTheme}>
<Box sx={{ display: "flex" }}>
<CssBaseline />
Expand All @@ -41,7 +51,11 @@ export default function App() {
}}
>
<Toolbar />
<Routes>{routesItems}</Routes>
<Routes>
{publicRoutesItems}
{privateRoutesItems}
</Routes>
<AlertComponent />
</Box>
</Box>
</ThemeProvider>
Expand Down
50 changes: 50 additions & 0 deletions src/webui/src/QueryClient.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { QueryClientProvider as ClientProvider, MutationCache, QueryCache, QueryClient } from "@tanstack/react-query";
import axios from "axios";
import { isUnauthorized } from "axiosInstance";
import { useNavigate } from "react-router-dom";
import { logout } from "queries/Auth";
import { useAlert } from "utils/AlertContext";

type Props = {
children: JSX.Element
};

export const QueryClientProvider = ({ children }: Props) => {
const { callAlert } = useAlert();
const navigate = useNavigate();

const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (error) => {
if (axios.isAxiosError(error)) {
if (isUnauthorized(error)) {
callAlert("error", `${error.response?.data}`);
logout(navigate);
}
}
}
}),
mutationCache: new MutationCache({
onSuccess: (_data, _variables, _context, mutation) => {
callAlert("success", "Success");
if (mutation.options.mutationKey) {
queryClient.invalidateQueries(mutation.options.mutationKey);
}
},
onError: (error) => {
if (axios.isAxiosError(error)) {
callAlert("error", `${error.response?.data}`);
if (isUnauthorized(error)) {
logout(navigate);
}
}
}
})
});

return (
<ClientProvider client={queryClient} contextSharing={true}>
{children}
</ClientProvider>
);
};
Loading
Loading