auth: Refactor home page

This commit is contained in:
Tom Wiesing 2023-01-02 14:23:10 +01:00
parent 2d5b92f464
commit b8f1281f78
No known key found for this signature in database
14 changed files with 222 additions and 129 deletions

View file

@ -2,16 +2,76 @@ package auth
import (
"context"
"errors"
"net/http"
"net/url"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
)
// Permission represents a permission for a user
// Permission represents a permission granted to a user.
//
// The nil permission represents any authenticated user.
type Permission func(user *AuthUser, r *http.Request) (ok bool, err error)
type Permission func(user *AuthUser, r *http.Request) (ok Grant, err error)
// Grant represents an object that either grants or denies access for a certain permission
type Grant interface {
isGranted()
// Granted returns a boolean indicating if permission to the resource in question
// has been granted
Granted() bool
// Denied returns a string containing an error message to display to the user when permission is denied.
// When Granted() returns true, the behaviour is undefined.
Denied() string
}
// Bool2Grant returns a new grant that returns granted for the given boolean, and message as the denied message.
func Bool2Grant(granted bool, message string) Grant {
if granted {
return grantAllow{}
}
return grantDeny(message)
}
type grantAllow struct{}
func (grantAllow) isGranted() {}
func (grantAllow) Granted() bool { return true }
func (grantAllow) Denied() string { return "" }
type grantDeny string
func (grantDeny) isGranted() {}
func (g grantDeny) Granted() bool { return false }
func (g grantDeny) Denied() string {
if g == "" {
return "Forbidden"
}
return string(g)
}
var errPermissionPanic = errors.New("permission: panic()")
// Permit checks if the given user has this permission.
func (perm Permission) Permit(user *AuthUser, r *http.Request) (ok Grant, err error) {
// if there is no permission, then we just check if there is some user
if perm == nil {
return Bool2Grant(user != nil, ""), nil
}
// recover any panic()ed permission call
// to prevent the handler from panic()ing
defer func() {
if p := recover(); p != nil {
ok = Bool2Grant(false, "unknown error")
err = errPermissionPanic
}
}()
return perm(user, r)
}
// Protect returns a new handler which requires a user to be logged in and pass the perm function.
//
@ -20,14 +80,15 @@ type Permission func(user *AuthUser, r *http.Request) (ok bool, err error)
// 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) {
var grant Grant
// 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 there is no user in the session, they need to login first!
if user == nil {
// we can't redirect anything other than GET
// (because it might be a form)
@ -42,31 +103,41 @@ func (auth *Auth) Protect(handler http.Handler, perm Permission) http.Handler {
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)
{
var err error
// call the permission check
grant, err = perm.Permit(user, r)
if err != nil {
goto err
}
if !ok {
if !grant.Granted() {
goto forbidden
}
}
// store the user into the session
// store the user into the session, and then return the new session
r = r.WithContext(context.WithValue(r.Context(), ctxUserKey, user))
handler.ServeHTTP(w, r)
return
forbidden:
httpx.HTMLInterceptor.Intercept(w, r, httpx.ErrForbidden)
return
{
message := "Forbidden"
if grant != nil {
message = grant.Denied()
}
httpx.Response{
ContentType: "text/plain",
StatusCode: http.StatusForbidden,
Body: []byte(message),
}.ServeHTTP(w, r)
return
}
err:
httpx.HTMLInterceptor.Fallback.ServeHTTP(w, r)
httpx.TextInterceptor.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
var Admin Permission = func(user *AuthUser, r *http.Request) (ok Grant, err error) {
return Bool2Grant(user != nil && user.Admin && user.TOTPEnabled, "user needs to have admin permissions and TOTP enabled"), nil
}

View file

@ -4,7 +4,6 @@ import (
"context"
_ "embed"
"errors"
"html/template"
"net/http"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static"
@ -29,11 +28,6 @@ func (auth *Auth) authHome(ctx context.Context) http.Handler {
var passwordHTMLString string
var passwordTemplate = static.AssetsAuthLogin.MustParseShared("password.html", passwordHTMLString)
type authpasswordContext struct {
Message string
Form template.HTML
}
var (
errPasswordsNotIdentical = errors.New("passwords are not identical")
errPasswordIsEmpty = errors.New("password is empty")
@ -55,16 +49,7 @@ func (auth *Auth) authPassword(ctx context.Context) http.Handler {
CSRF: auth.csrf.Get(nil),
RenderForm: func(template template.HTML, err error, w http.ResponseWriter, r *http.Request) {
ctx := authpasswordContext{
Message: "",
Form: template,
}
if err != nil {
ctx.Message = err.Error()
}
httpx.WriteHTML(ctx, nil, passwordTemplate, "", w, r)
},
RenderTemplate: passwordTemplate,
Validate: func(r *http.Request, values map[string]string) (struct{}, error) {
old, passcode, new, new2 := values["old"], values["passcode"], values["new"], values["new2"]

View file

@ -2,7 +2,7 @@ package auth
import (
"context"
"html/template"
"errors"
"net/http"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static"
@ -110,10 +110,7 @@ var loginResponse = httpx.Response{
Body: []byte("user is signed in"),
}
type authloginContext struct {
Message string
Form template.HTML
}
var errLoginFailed = errors.New("Login failed")
// authLogin implements a view to login a user
func (auth *Auth) authLogin(ctx context.Context) http.Handler {
@ -127,15 +124,11 @@ func (auth *Auth) authLogin(ctx context.Context) http.Handler {
CSRF: auth.csrf.Get(nil),
RenderForm: func(template template.HTML, err error, w http.ResponseWriter, r *http.Request) {
ctx := authloginContext{
Message: "",
Form: template,
RenderForm: func(context httpx.FormContext, w http.ResponseWriter, r *http.Request) {
if context.Err != nil {
context.Err = errLoginFailed
}
if err != nil {
ctx.Message = "Login Failed"
}
httpx.WriteHTML(ctx, nil, loginTemplate, "", w, r)
httpx.WriteHTML(context, nil, loginTemplate, "", w, r)
},
Validate: func(r *http.Request, values map[string]string) (*AuthUser, error) {

View file

@ -10,10 +10,33 @@
{{ define "content" }}
<div class="pure-u-1">
<div>
Welcome {{ .User.User }}!
<hr />
Welcome {{ .User.User }}!
<a class="pure-button pure-button-small" href="/auth/logout/">Logout</a>
<hr />
</div>
<div class="pure-u-1">
<p>
{{ if .User.Admin }}
You are an administrator.
{{ else }}
You are a regular user.
{{ end }}
{{ if .User.TOTPEnabled }}
You have TOTP enabled.
{{ else }}
You do not have TOTP enabled.
{{ end }}
</p>
<div class="pure-button-group" role="group" role="Actions">
<a class="pure-button" href="/auth/password/">Change Password</a>
{{ if .User.TOTPEnabled }}
<a class="pure-button" href="/auth/totp/disable/">Disable TOTP</a>
{{ else }}
<a class="pure-button" href="/auth/totp/enable/">Enable TOTP</a>
{{ end }}
</div>
<hr />
</div>
{{ if .User.Admin }}
@ -31,21 +54,8 @@
{{ end }}
<div class="pure-u-1">
<div class="pure-button-group" role="group" role="Actions">
<a class="pure-button" href="/auth/password/">Change Password</a>
{{ if .User.TOTPEnabled }}
<a class="pure-button" href="/auth/totp/disable/">Disable TOTP</a>
{{ else }}
<a class="pure-button" href="/auth/totp/enable/">Enable TOTP</a>
{{ end }}
</div>
<hr />
There will be a list of WissKIs you have access to here.
</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 }}

View file

@ -13,7 +13,7 @@
<img src="{{ .TOTPImage }}" alt="TOTP Enrollment Image">
</a>
<ul>
<li>scan the token above using a <a href="https://en.wikipedia.org/wiki/Time-based_one-time_password">TOTP</a>app on your phone</li>
<li>scan the token above using a <a href="https://en.wikipedia.org/wiki/Time-based_one-time_password">TOTP</a> app on your phone</li>
<li>enter your current password and the now generated token to confirm</li>
</ul>
</div>

View file

@ -11,11 +11,6 @@ import (
_ "embed"
)
type totpContext struct {
Message string
Form template.HTML
}
//go:embed "templates/totp_enable.html"
var totpEnableStr string
var totpEnableTemplate = static.AssetsAuthLogin.MustParseShared("totp_enable.html", totpEnableStr)
@ -30,19 +25,10 @@ func (auth *Auth) authTOTPEnable(ctx context.Context) http.Handler {
CSRF: auth.csrf.Get(nil),
SkipForm: func(r *http.Request) (data struct{}, skip bool) {
user, _ := auth.UserOf(r)
return struct{}{}, user != nil && user.TOTPEnabled
},
RenderForm: func(template template.HTML, err error, w http.ResponseWriter, r *http.Request) {
ctx := totpContext{
Message: "",
Form: template,
}
if err != nil {
ctx.Message = err.Error()
}
httpx.WriteHTML(ctx, nil, totpEnableTemplate, "", w, r)
user, err := auth.UserOf(r)
return struct{}{}, err == nil && user != nil && user.TOTPEnabled
},
RenderTemplate: totpEnableTemplate,
Validate: func(r *http.Request, values map[string]string) (struct{}, error) {
password := values["password"]
@ -80,7 +66,7 @@ var totpEnrollStr string
var totpEnrollTemplate = static.AssetsAuthLogin.MustParseShared("totp_enroll.html", totpEnrollStr)
type totpEnrollContext struct {
totpContext
httpx.FormContext
TOTPImage template.URL
TOTPURL template.URL
}
@ -99,12 +85,9 @@ func (auth *Auth) authTOTPEnroll(ctx context.Context) http.Handler {
user, _ := auth.UserOf(r)
return struct{}{}, user != nil && user.TOTPEnabled
},
RenderForm: func(tpl template.HTML, err error, w http.ResponseWriter, r *http.Request) {
RenderForm: func(context httpx.FormContext, w http.ResponseWriter, r *http.Request) {
ctx := totpEnrollContext{
totpContext: totpContext{
Message: "",
Form: tpl,
},
FormContext: context,
}
if user, err := auth.UserOf(r); err == nil && user != nil {
@ -116,9 +99,6 @@ func (auth *Auth) authTOTPEnroll(ctx context.Context) http.Handler {
ctx.TOTPURL = template.URL(secret.URL())
}
}
if err != nil {
ctx.Message = err.Error()
}
httpx.WriteHTML(ctx, nil, totpEnrollTemplate, "", w, r)
},
@ -171,16 +151,7 @@ func (auth *Auth) authTOTPDisable(ctx context.Context) http.Handler {
user, _ := auth.UserOf(r)
return struct{}{}, user != nil && !user.TOTPEnabled
},
RenderForm: func(template template.HTML, err error, w http.ResponseWriter, r *http.Request) {
ctx := totpContext{
Message: "",
Form: template,
}
if err != nil {
ctx.Message = err.Error()
}
httpx.WriteHTML(ctx, nil, totpDisableTemplate, "", w, r)
},
RenderTemplate: totpDisableTemplate,
Validate: func(r *http.Request, values map[string]string) (struct{}, error) {
password, passcode := values["password"], values["passcode"]

View file

@ -5,47 +5,47 @@ package static
// AssetsHomeHome contains assets for the 'HomeHome' entrypoint.
var AssetsHomeHome = Assets{
Scripts: `<script type="module" src="/static/HomeHome.38d394c2.js"></script><script src="/static/HomeHome.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/HomeHome.38d394c2.js"></script><script src="/static/HomeHome.38d394c2.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/static/HomeHome.a75f04fa.css"><link rel="stylesheet" href="/static/HomeHome.2353e048.css">`,
Styles: `<link rel="stylesheet" href="/static/HomeHome.25c6db8a.css"><link rel="stylesheet" href="/static/HomeHome.2353e048.css">`,
}
// AssetsComponentsIndex contains assets for the 'ComponentsIndex' entrypoint.
var AssetsComponentsIndex = Assets{
Scripts: `<script type="module" src="/static/HomeHome.38d394c2.js"></script><script src="/static/HomeHome.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/ComponentsIndex.38d394c2.js"></script><script src="/static/ComponentsIndex.38d394c2.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/static/HomeHome.a75f04fa.css"><link rel="stylesheet" href="/static/ComponentsIndex.38d394c2.css">`,
Styles: `<link rel="stylesheet" href="/static/HomeHome.25c6db8a.css"><link rel="stylesheet" href="/static/ComponentsIndex.38d394c2.css">`,
}
// AssetsControlIndex contains assets for the 'ControlIndex' entrypoint.
var AssetsControlIndex = Assets{
Scripts: `<script type="module" src="/static/HomeHome.38d394c2.js"></script><script src="/static/HomeHome.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/ControlIndex.a72fc239.js"></script><script src="/static/ControlIndex.75d2a312.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/static/HomeHome.a75f04fa.css"><link rel="stylesheet" href="/static/ControlIndex.6d59e220.css"><link rel="stylesheet" href="/static/ControlIndex.6d2ae968.css">`,
Styles: `<link rel="stylesheet" href="/static/HomeHome.25c6db8a.css"><link rel="stylesheet" href="/static/ControlIndex.6d59e220.css"><link rel="stylesheet" href="/static/ControlIndex.6d2ae968.css">`,
}
// AssetsControlInstance contains assets for the 'ControlInstance' entrypoint.
var AssetsControlInstance = Assets{
Scripts: `<script nomodule="" defer src="/static/ControlIndex.75d2a312.js"></script><script type="module" src="/static/ControlIndex.a72fc239.js"></script><script type="module" src="/static/HomeHome.38d394c2.js"></script><script src="/static/HomeHome.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/ControlInstance.66b95713.js"></script><script src="/static/ControlInstance.9cc7166d.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/static/HomeHome.a75f04fa.css"><link rel="stylesheet" href="/static/ControlIndex.6d59e220.css"><link rel="stylesheet" href="/static/ControlIndex.6d2ae968.css"><link rel="stylesheet" href="/static/ControlInstance.38d394c2.css">`,
Styles: `<link rel="stylesheet" href="/static/HomeHome.25c6db8a.css"><link rel="stylesheet" href="/static/ControlIndex.6d59e220.css"><link rel="stylesheet" href="/static/ControlIndex.6d2ae968.css"><link rel="stylesheet" href="/static/ControlInstance.38d394c2.css">`,
}
// AssetsInstanceComponentsIndex contains assets for the 'InstanceComponentsIndex' entrypoint.
var AssetsInstanceComponentsIndex = Assets{
Scripts: `<script type="module" src="/static/HomeHome.38d394c2.js"></script><script src="/static/HomeHome.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/InstanceComponentsIndex.38d394c2.js"></script><script src="/static/InstanceComponentsIndex.38d394c2.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/static/HomeHome.a75f04fa.css"><link rel="stylesheet" href="/static/InstanceComponentsIndex.38d394c2.css">`,
Styles: `<link rel="stylesheet" href="/static/HomeHome.25c6db8a.css"><link rel="stylesheet" href="/static/InstanceComponentsIndex.38d394c2.css">`,
}
// AssetsAuthLogin contains assets for the 'AuthLogin' entrypoint.
var AssetsAuthLogin = Assets{
Scripts: `<script type="module" src="/static/HomeHome.38d394c2.js"></script><script src="/static/HomeHome.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/AuthLogin.38d394c2.js"></script><script src="/static/AuthLogin.38d394c2.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/static/HomeHome.a75f04fa.css"><link rel="stylesheet" href="/static/AuthLogin.38d394c2.css">`,
Styles: `<link rel="stylesheet" href="/static/HomeHome.25c6db8a.css"><link rel="stylesheet" href="/static/AuthLogin.38d394c2.css">`,
}
// AssetsAuthHome contains assets for the 'AuthHome' entrypoint.
var AssetsAuthHome = Assets{
Scripts: `<script type="module" src="/static/HomeHome.38d394c2.js"></script><script src="/static/HomeHome.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/AuthHome.38d394c2.js"></script><script src="/static/AuthHome.38d394c2.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/static/HomeHome.a75f04fa.css"><link rel="stylesheet" href="/static/AuthHome.38d394c2.css">`,
Styles: `<link rel="stylesheet" href="/static/HomeHome.25c6db8a.css"><link rel="stylesheet" href="/static/AuthHome.38d394c2.css">`,
}
// AssetsAuthTOTP contains assets for the 'AuthTOTP' entrypoint.
var AssetsAuthTOTP = Assets{
Scripts: `<script type="module" src="/static/HomeHome.38d394c2.js"></script><script src="/static/HomeHome.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/AuthTOTP.38d394c2.js"></script><script src="/static/AuthTOTP.38d394c2.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/static/HomeHome.a75f04fa.css"><link rel="stylesheet" href="/static/AuthTOTP.38d394c2.css">`,
Styles: `<link rel="stylesheet" href="/static/HomeHome.25c6db8a.css"><link rel="stylesheet" href="/static/AuthTOTP.38d394c2.css">`,
}

View file

@ -47,4 +47,20 @@ footer {
}
.pure-button-success {
background-color: rgb(28, 184, 65) !important;
}
.pure-button-xsmall {
font-size: 70%;
}
.pure-button-small {
font-size: 85%;
}
.pure-button-large {
font-size: 110%;
}
.pure-button-xlarge {
font-size: 125%;
}

View file

@ -12,9 +12,10 @@
<div class="pure-u-1">
{{ block "form/message" . }}
{{ if .Message }}
{{ $E := .Error }}
{{ if not (eq $E "") }}
<div>
{{ .Message }}
{{ $E }}
</div>
{{ end }}
{{ end }}

View file

@ -26,7 +26,7 @@ type Storage struct {
// Get retrieves metadata with the provided key and deserializes the first one into target.
// If no metadatum exists, returns [ErrMetadatumNotSet].
func (s Storage) Get(ctx context.Context, key Key, target any) error {
table, err := s.sql.QueryTable(ctx, true, models.MetadataTable)
table, err := s.sql.QueryTable(ctx, true, models.AccessTable)
if err != nil {
return err
}
@ -55,7 +55,7 @@ func (s Storage) Get(ctx context.Context, key Key, target any) error {
//
// When no metadatum exists, targets is not called, and nil error is returned.
func (s Storage) GetAll(ctx context.Context, key Key, target func(index, total int) any) error {
table, err := s.sql.QueryTable(ctx, true, models.MetadataTable)
table, err := s.sql.QueryTable(ctx, true, models.AccessTable)
if err != nil {
return err
}
@ -82,7 +82,7 @@ func (s Storage) GetAll(ctx context.Context, key Key, target func(index, total i
// Delete deletes all metadata with the provided key.
func (s Storage) Delete(ctx context.Context, key Key) error {
table, err := s.sql.QueryTable(ctx, true, models.MetadataTable)
table, err := s.sql.QueryTable(ctx, true, models.AccessTable)
if err != nil {
return err
}
@ -98,7 +98,7 @@ func (s Storage) Delete(ctx context.Context, key Key) error {
// Set serializes value and stores it with the provided key.
// Any other metadata with the same key is deleted.
func (s Storage) Set(ctx context.Context, key Key, value any) error {
table, err := s.sql.QueryTable(ctx, true, models.MetadataTable)
table, err := s.sql.QueryTable(ctx, true, models.AccessTable)
if err != nil {
return err
}
@ -133,7 +133,7 @@ func (s Storage) Set(ctx context.Context, key Key, value any) error {
// Set serializes values and stores them with the provided key.
// Any other metadata with the same key is deleted.
func (s Storage) SetAll(ctx context.Context, key Key, values ...any) error {
table, err := s.sql.QueryTable(ctx, true, models.MetadataTable)
table, err := s.sql.QueryTable(ctx, true, models.AccessTable)
if err != nil {
return err
}
@ -167,7 +167,7 @@ func (s Storage) SetAll(ctx context.Context, key Key, values ...any) error {
// Purge removes all metadata, regardless of key.
func (s Storage) Purge(ctx context.Context) error {
table, err := s.sql.QueryTable(ctx, true, models.MetadataTable)
table, err := s.sql.QueryTable(ctx, true, models.AccessTable)
if err != nil {
return err
}

View file

@ -91,7 +91,7 @@ func (sql *SQL) Update(ctx context.Context, progress io.Writer) error {
{
"metadata",
&models.Metadatum{},
models.MetadataTable,
models.AccessTable,
},
{
"snapshot",