Use authentication for Distillery control page
This commit is contained in:
parent
da32b67981
commit
1caecc0f19
8 changed files with 122 additions and 82 deletions
|
|
@ -14,6 +14,9 @@ type disUser struct {
|
||||||
CreateUser bool `short:"c" long:"create" description:"create a new user"`
|
CreateUser bool `short:"c" long:"create" description:"create a new user"`
|
||||||
DeleteUser bool `short:"d" long:"delete" description:"delete a 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"`
|
InfoUser bool `short:"i" long:"info" description:"show information about a user"`
|
||||||
ListUsers bool `short:"l" long:"list" description:"list all users"`
|
ListUsers bool `short:"l" long:"list" description:"list all users"`
|
||||||
|
|
||||||
|
|
@ -56,6 +59,8 @@ func (du disUser) AfterParse() error {
|
||||||
du.ListUsers,
|
du.ListUsers,
|
||||||
du.DisableTOTP,
|
du.DisableTOTP,
|
||||||
du.EnableTOTP,
|
du.EnableTOTP,
|
||||||
|
du.MakeAdmin,
|
||||||
|
du.RemoveAdmin,
|
||||||
} {
|
} {
|
||||||
if action {
|
if action {
|
||||||
counter++
|
counter++
|
||||||
|
|
@ -93,6 +98,10 @@ func (du disUser) Run(context wisski_distillery.Context) error {
|
||||||
return du.runEnableTOTP(context)
|
return du.runEnableTOTP(context)
|
||||||
case du.DisableTOTP:
|
case du.DisableTOTP:
|
||||||
return du.runDisableTOTP(context)
|
return du.runDisableTOTP(context)
|
||||||
|
case du.MakeAdmin:
|
||||||
|
return du.runMakeAdmin(context)
|
||||||
|
case du.RemoveAdmin:
|
||||||
|
return du.runRemoveAdmin(context)
|
||||||
}
|
}
|
||||||
panic("never reached")
|
panic("never reached")
|
||||||
}
|
}
|
||||||
|
|
@ -246,3 +255,23 @@ func (du disUser) runDisableTOTP(context wisski_distillery.Context) error {
|
||||||
|
|
||||||
return user.DisableTOTP(context.Context)
|
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)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -84,11 +84,6 @@ type Config struct {
|
||||||
// admin credentials for the Mysql database
|
// admin credentials for the Mysql database
|
||||||
MysqlAdminUser string `env:"MYSQL_ADMIN_USER" default:"admin" parser:"nonempty"`
|
MysqlAdminUser string `env:"MYSQL_ADMIN_USER" default:"admin" parser:"nonempty"`
|
||||||
MysqlAdminPassword string `env:"MYSQL_ADMIN_PASSWORD" default:"" 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
|
// session secret holds the secret for login
|
||||||
SessionSecret string `env:"SESSION_SECRET" default:"" parser:"nonempty"`
|
SessionSecret string `env:"SESSION_SECRET" default:"" parser:"nonempty"`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -69,11 +69,6 @@ GRAPHDB_ADMIN_PASSWORD=${GRAPHDB_ADMIN_PASSWORD}
|
||||||
MYSQL_ADMIN_USER=${MYSQL_ADMIN_USER}
|
MYSQL_ADMIN_USER=${MYSQL_ADMIN_USER}
|
||||||
MYSQL_ADMIN_PASSWORD=${MYSQL_ADMIN_PASSWORD}
|
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
|
# the interval to run cron in
|
||||||
CRON_INTERVAL=10m
|
CRON_INTERVAL=10m
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,6 @@ type Template struct {
|
||||||
TriplestoreAdminPassword string `env:"GRAPHDB_ADMIN_PASSWORD"`
|
TriplestoreAdminPassword string `env:"GRAPHDB_ADMIN_PASSWORD"`
|
||||||
MysqlAdminUsername string `env:"MYSQL_ADMIN_USER"`
|
MysqlAdminUsername string `env:"MYSQL_ADMIN_USER"`
|
||||||
MysqlAdminPassword string `env:"MYSQL_ADMIN_PASSWORD"`
|
MysqlAdminPassword string `env:"MYSQL_ADMIN_PASSWORD"`
|
||||||
DisAdminUsername string `env:"DIS_ADMIN_USER"`
|
|
||||||
DisAdminPassword string `env:"DIS_ADMIN_PASSWORD"`
|
|
||||||
DockerNetworkName string `env:"DOCKER_NETWORK_NAME"`
|
DockerNetworkName string `env:"DOCKER_NETWORK_NAME"`
|
||||||
SessionSecret string `env:"SESSION_SECRET"`
|
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 == "" {
|
if tpl.DockerNetworkName == "" {
|
||||||
tpl.DockerNetworkName, err = password.Password(10)
|
tpl.DockerNetworkName, err = password.Password(10)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
72
internal/dis/component/auth/protect.go
Normal file
72
internal/dis/component/auth/protect.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
|
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static"
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static"
|
||||||
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
|
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
|
||||||
|
|
@ -116,58 +115,6 @@ type authloginContext struct {
|
||||||
Form template.HTML
|
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
|
// authLogin implements a view to login a user
|
||||||
func (auth *Auth) authLogin(ctx context.Context) http.Handler {
|
func (auth *Auth) authLogin(ctx context.Context) http.Handler {
|
||||||
return &httpx.Form[*AuthUser]{
|
return &httpx.Form[*AuthUser]{
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,19 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{ if .User.Admin }}
|
||||||
<div class="pure-u-1">
|
<div class="pure-u-1">
|
||||||
<div class="pure-button-group" role="group" role="Actions">
|
{{ if (not .User.TOTPEnabled) }}
|
||||||
<a class="pure-button" href="/auth/logout/">Logout</a>
|
<div>
|
||||||
|
TOTP is required to access these.
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
{{ end }}
|
||||||
|
<div class="pure-button-group" role="group" role="Actions">
|
||||||
|
<a class="pure-button" href="/dis/">Distillery Control Page</a>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
</div>
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
<div class="pure-u-1">
|
<div class="pure-u-1">
|
||||||
<div class="pure-button-group" role="group" role="Actions">
|
<div class="pure-button-group" role="group" role="Actions">
|
||||||
|
|
@ -32,6 +39,13 @@
|
||||||
<a class="pure-button" href="/auth/totp/enable/">Enable TOTP</a>
|
<a class="pure-button" href="/auth/totp/enable/">Enable TOTP</a>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
|
<hr />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-u-1">
|
||||||
|
<div class="pure-button-group" role="group" role="Actions">
|
||||||
|
<a class="pure-button" href="/auth/logout/">Logout</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
|
"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"
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/exporter/logger"
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/exporter/logger"
|
||||||
"github.com/julienschmidt/httprouter"
|
"github.com/julienschmidt/httprouter"
|
||||||
|
|
@ -22,6 +23,8 @@ type Info struct {
|
||||||
Exporter *exporter.Exporter
|
Exporter *exporter.Exporter
|
||||||
Instances *instances.Instances
|
Instances *instances.Instances
|
||||||
SnapshotsLog *logger.Logger
|
SnapshotsLog *logger.Logger
|
||||||
|
|
||||||
|
Auth *auth.Auth
|
||||||
}
|
}
|
||||||
|
|
||||||
Analytics *lazy.PoolAnalytics
|
Analytics *lazy.PoolAnalytics
|
||||||
|
|
@ -44,9 +47,7 @@ func (info *Info) HandleRoute(ctx context.Context, route string) (handler http.H
|
||||||
Fallback: router,
|
Fallback: router,
|
||||||
Handler: info.serveSocket,
|
Handler: info.serveSocket,
|
||||||
}
|
}
|
||||||
handler = httpx.BasicAuth(socket, "WissKI Distillery Admin", func(user, pass string) bool {
|
handler = info.Dependencies.Auth.Protect(socket, auth.Admin)
|
||||||
return user == info.Config.DisAdminUser && pass == info.Config.DisAdminPassword
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle everything
|
// handle everything
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue