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"`
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
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"
|
||||
"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]{
|
||||
|
|
|
|||
|
|
@ -16,12 +16,19 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{{ if .User.Admin }}
|
||||
<div class="pure-u-1">
|
||||
<div class="pure-button-group" role="group" role="Actions">
|
||||
<a class="pure-button" href="/auth/logout/">Logout</a>
|
||||
{{ if (not .User.TOTPEnabled) }}
|
||||
<div>
|
||||
TOTP is required to access these.
|
||||
</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>
|
||||
{{ end }}
|
||||
|
||||
<div class="pure-u-1">
|
||||
<div class="pure-button-group" role="group" role="Actions">
|
||||
|
|
@ -32,6 +39,13 @@
|
|||
<a class="pure-button" href="/auth/totp/enable/">Enable TOTP</a>
|
||||
{{ end }}
|
||||
</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>
|
||||
|
||||
{{ end }}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue