From 1caecc0f1950e6275b51744b124f785482fe627a Mon Sep 17 00:00:00 2001 From: Tom Wiesing Date: Thu, 29 Dec 2022 16:34:45 +0100 Subject: [PATCH] Use authentication for Distillery control page --- cmd/dis_user.go | 29 ++++++++ internal/config/config.go | 5 -- internal/config/config_template | 5 -- internal/config/template.go | 13 ---- internal/dis/component/auth/protect.go | 72 +++++++++++++++++++ internal/dis/component/auth/session.go | 53 -------------- .../dis/component/auth/templates/home.html | 20 +++++- internal/dis/component/control/info/info.go | 7 +- 8 files changed, 122 insertions(+), 82 deletions(-) create mode 100644 internal/dis/component/auth/protect.go diff --git a/cmd/dis_user.go b/cmd/dis_user.go index b3359bd..e051218 100644 --- a/cmd/dis_user.go +++ b/cmd/dis_user.go @@ -14,6 +14,9 @@ type disUser struct { CreateUser bool `short:"c" long:"create" description:"create a new user"` DeleteUser bool `short:"d" long:"delete" description:"delete a user"` + MakeAdmin bool `short:"a" long:"add-admin" description:"add admin permission to user"` + RemoveAdmin bool `short:"A" long:"remove-admin" description:"remove admin permission from user"` + InfoUser bool `short:"i" long:"info" description:"show information about a user"` ListUsers bool `short:"l" long:"list" description:"list all users"` @@ -56,6 +59,8 @@ func (du disUser) AfterParse() error { du.ListUsers, du.DisableTOTP, du.EnableTOTP, + du.MakeAdmin, + du.RemoveAdmin, } { if action { counter++ @@ -93,6 +98,10 @@ func (du disUser) Run(context wisski_distillery.Context) error { return du.runEnableTOTP(context) case du.DisableTOTP: return du.runDisableTOTP(context) + case du.MakeAdmin: + return du.runMakeAdmin(context) + case du.RemoveAdmin: + return du.runRemoveAdmin(context) } panic("never reached") } @@ -246,3 +255,23 @@ func (du disUser) runDisableTOTP(context wisski_distillery.Context) error { return user.DisableTOTP(context.Context) } + +func (du disUser) runMakeAdmin(context wisski_distillery.Context) error { + user, err := context.Environment.Auth().User(context.Context, du.Positionals.User) + if err != nil { + return err + } + + user.Admin = true + return user.Save(context.Context) +} + +func (du disUser) runRemoveAdmin(context wisski_distillery.Context) error { + user, err := context.Environment.Auth().User(context.Context, du.Positionals.User) + if err != nil { + return err + } + + user.Admin = false + return user.Save(context.Context) +} diff --git a/internal/config/config.go b/internal/config/config.go index 595d548..b4d7464 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -84,11 +84,6 @@ type Config struct { // admin credentials for the Mysql database MysqlAdminUser string `env:"MYSQL_ADMIN_USER" default:"admin" parser:"nonempty"` MysqlAdminPassword string `env:"MYSQL_ADMIN_PASSWORD" default:"" parser:"nonempty"` - - // admin credentials for the dis server - DisAdminUser string `env:"DIS_ADMIN_USER" default:"admin" parser:"nonempty"` - DisAdminPassword string `env:"DIS_ADMIN_PASSWORD" default:"" parser:"nonempty"` - // session secret holds the secret for login SessionSecret string `env:"SESSION_SECRET" default:"" parser:"nonempty"` diff --git a/internal/config/config_template b/internal/config/config_template index 9d4d7bd..435c51b 100644 --- a/internal/config/config_template +++ b/internal/config/config_template @@ -69,11 +69,6 @@ GRAPHDB_ADMIN_PASSWORD=${GRAPHDB_ADMIN_PASSWORD} MYSQL_ADMIN_USER=${MYSQL_ADMIN_USER} MYSQL_ADMIN_PASSWORD=${MYSQL_ADMIN_PASSWORD} - -# The admin user and password required to access the /dis/ server and api -DIS_ADMIN_USER=${DIS_ADMIN_USER} -DIS_ADMIN_PASSWORD=${DIS_ADMIN_PASSWORD} - # the interval to run cron in CRON_INTERVAL=10m diff --git a/internal/config/template.go b/internal/config/template.go index 0b08e95..7d8aaff 100644 --- a/internal/config/template.go +++ b/internal/config/template.go @@ -26,8 +26,6 @@ type Template struct { TriplestoreAdminPassword string `env:"GRAPHDB_ADMIN_PASSWORD"` MysqlAdminUsername string `env:"MYSQL_ADMIN_USER"` MysqlAdminPassword string `env:"MYSQL_ADMIN_PASSWORD"` - DisAdminUsername string `env:"DIS_ADMIN_USER"` - DisAdminPassword string `env:"DIS_ADMIN_PASSWORD"` DockerNetworkName string `env:"DOCKER_NETWORK_NAME"` SessionSecret string `env:"SESSION_SECRET"` } @@ -76,17 +74,6 @@ func (tpl *Template) SetDefaults(env environment.Environment) (err error) { } } - if tpl.DisAdminUsername == "" { - tpl.DisAdminUsername = "admin" - } - - if tpl.DisAdminPassword == "" { - tpl.DisAdminPassword, err = password.Password(64) - if err != nil { - return err - } - } - if tpl.DockerNetworkName == "" { tpl.DockerNetworkName, err = password.Password(10) if err != nil { diff --git a/internal/dis/component/auth/protect.go b/internal/dis/component/auth/protect.go new file mode 100644 index 0000000..27cc503 --- /dev/null +++ b/internal/dis/component/auth/protect.go @@ -0,0 +1,72 @@ +package auth + +import ( + "context" + "net/http" + "net/url" + + "github.com/FAU-CDI/wisski-distillery/pkg/httpx" +) + +// Permission represents a permission for a user +// +// The nil permission represents any authenticated user. +type Permission func(user *AuthUser, r *http.Request) (ok bool, err error) + +// Protect returns a new handler which requires a user to be logged in and pass the perm function. +// +// If an unauthenticated user attempts to access the returned handler, they are redirected to the login endpoint. +// If an authenticated user is missing permissions, a Forbidden response is called. +// If an authenticated calls the endpoint, and they have the given permissions, the original handler is called. +func (auth *Auth) Protect(handler http.Handler, perm Permission) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // load the user in the session + user, err := auth.UserOf(r) + if err != nil { + goto err + } + + // if there is no user in the session + // we need to login the user + if user == nil { + // we can't redirect anything other than GET + // (because it might be a form) + // => so we just return a forbidden + if r.Method != http.MethodGet { + goto forbidden + } + + // redirect the user to the login endpoint, with the original URI as a return + dest := "/auth/login?next=" + url.QueryEscape(r.URL.RequestURI()) + http.Redirect(w, r, dest, http.StatusSeeOther) + return + } + + // if we have a permission check, we need to call it + // to find out if the user is actually allowed to access the page + if perm != nil { + ok, err := perm(user, r) + if err != nil { + goto err + } + if !ok { + goto forbidden + } + } + + // store the user into the session + r = r.WithContext(context.WithValue(r.Context(), ctxUserKey, user)) + handler.ServeHTTP(w, r) + return + forbidden: + httpx.HTMLInterceptor.Intercept(w, r, httpx.ErrForbidden) + return + err: + httpx.HTMLInterceptor.Fallback.ServeHTTP(w, r) + }) +} + +// Admin represents a permission that checks if a user is an administrator and has totp enabled. +var Admin Permission = func(user *AuthUser, r *http.Request) (ok bool, err error) { + return user != nil && user.Admin && user.TOTPEnabled, nil +} diff --git a/internal/dis/component/auth/session.go b/internal/dis/component/auth/session.go index 475415a..ae119b6 100644 --- a/internal/dis/component/auth/session.go +++ b/internal/dis/component/auth/session.go @@ -4,7 +4,6 @@ import ( "context" "html/template" "net/http" - "net/url" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static" "github.com/FAU-CDI/wisski-distillery/pkg/httpx" @@ -116,58 +115,6 @@ type authloginContext struct { Form template.HTML } -// Protect returns a new handler which requires a user to be logged in and pass the perm function. -// -// If an unauthenticated user attempts to access the returned handler, they are redirected to the login endpoint. -// When a user is logged in, and they pass the perm function (or the perm function is nil), the original handler is called. -func (auth *Auth) Protect(handler http.Handler, perm func(user *AuthUser, r *http.Request) (ok bool, err error)) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // load the user in the session - user, err := auth.UserOf(r) - if err != nil { - goto err - } - - // if there is no user in the session - // we need to login the user - if user == nil { - // we can't redirect anything other than GET - // (because it might be a form) - // => so we just return a forbidden - if r.Method != http.MethodGet { - goto forbidden - } - - // redirect the user to the login endpoint, with the original URI as a return - dest := "/auth/login?next=" + url.QueryEscape(r.URL.RequestURI()) - http.Redirect(w, r, dest, http.StatusSeeOther) - return - } - - // if we have a permission check, we need to call it - // to find out if the user is actually allowed to access the page - if perm != nil { - ok, err := perm(user, r) - if err != nil { - goto err - } - if !ok { - goto forbidden - } - } - - // store the user into the session - r = r.WithContext(context.WithValue(r.Context(), ctxUserKey, user)) - handler.ServeHTTP(w, r) - return - forbidden: - httpx.HTMLInterceptor.Intercept(w, r, httpx.ErrForbidden) - return - err: - httpx.HTMLInterceptor.Fallback.ServeHTTP(w, r) - }) -} - // authLogin implements a view to login a user func (auth *Auth) authLogin(ctx context.Context) http.Handler { return &httpx.Form[*AuthUser]{ diff --git a/internal/dis/component/auth/templates/home.html b/internal/dis/component/auth/templates/home.html index 20a5f3d..74ad1f5 100644 --- a/internal/dis/component/auth/templates/home.html +++ b/internal/dis/component/auth/templates/home.html @@ -16,12 +16,19 @@ +{{ if .User.Admin }}
-
- Logout + {{ if (not .User.TOTPEnabled) }} +
+ TOTP is required to access these.
-
+ {{ end }} + +
+{{ end }}
@@ -32,6 +39,13 @@ Enable TOTP {{ end }}
+
+
+ +
+
+ Logout +
{{ end }} \ No newline at end of file diff --git a/internal/dis/component/control/info/info.go b/internal/dis/component/control/info/info.go index 1fb4d71..66b931a 100644 --- a/internal/dis/component/control/info/info.go +++ b/internal/dis/component/control/info/info.go @@ -5,6 +5,7 @@ import ( "net/http" "github.com/FAU-CDI/wisski-distillery/internal/dis/component" + "github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/exporter" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/exporter/logger" "github.com/julienschmidt/httprouter" @@ -22,6 +23,8 @@ type Info struct { Exporter *exporter.Exporter Instances *instances.Instances SnapshotsLog *logger.Logger + + Auth *auth.Auth } Analytics *lazy.PoolAnalytics @@ -44,9 +47,7 @@ func (info *Info) HandleRoute(ctx context.Context, route string) (handler http.H Fallback: router, Handler: info.serveSocket, } - handler = httpx.BasicAuth(socket, "WissKI Distillery Admin", func(user, pass string) bool { - return user == info.Config.DisAdminUser && pass == info.Config.DisAdminPassword - }) + handler = info.Dependencies.Auth.Protect(socket, auth.Admin) } // handle everything