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

File diff suppressed because one or more lines are too long

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",