WIPL tokens

This commit is contained in:
Tom 2023-06-06 18:26:53 +02:00
parent c09c729157
commit 161e08fe1f
25 changed files with 716 additions and 63 deletions

View file

@ -9,6 +9,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/next"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/policy"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/scopes"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/tokens"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/ssh2"
@ -24,6 +25,7 @@ type UserPanel struct {
Auth *auth.Auth
Templating *templating.Templating
Policy *policy.Policy
Tokens *tokens.Tokens
Instances *instances.Instances
Next *next.Next
Keys *sshkeys.SSHKeys
@ -40,14 +42,14 @@ func (panel *UserPanel) Routes() component.Routes {
return component.Routes{
Prefix: "/user/",
CSRF: true,
Decorator: panel.Dependencies.Auth.Require(scopes.ScopeUserLoggedIn, nil),
Decorator: panel.Dependencies.Auth.Require(false, scopes.ScopeUserLoggedIn, nil),
}
}
func (panel *UserPanel) Menu(r *http.Request) []component.MenuItem {
title := "Login"
user, err := panel.Dependencies.Auth.UserOf(r)
user, err := panel.Dependencies.Auth.UserOfSession(r)
if user != nil && err == nil {
title = user.User.User
}
@ -62,6 +64,9 @@ var (
menuSSH = component.MenuItem{Title: "SSH Keys", Path: "/user/ssh/"}
menuSSHAdd = component.MenuItem{Title: "Add New Key", Path: "/user/ssh/add/"}
menuTokens = component.MenuItem{Title: "Tokens", Path: "/user/tokens/"}
menuTokensAdd = component.MenuItem{Title: "Add New Token", Path: "/user/tokens/add/"}
menuTOTPAction = component.DummyMenuItem()
menuTOTPDisable = component.MenuItem{Title: "Disable Passcode (TOTP)", Path: "/user/totp/disable/"}
menuTOTPEnable = component.MenuItem{Title: "Enable Passcode (TOTP)", Path: "/user/totp/enable/"}
@ -115,8 +120,24 @@ func (panel *UserPanel) HandleRoute(ctx context.Context, route string) (http.Han
router.Handler(http.MethodPost, route+"ssh/delete", delete)
}
{
tokens := panel.tokensRoute(ctx)
router.Handler(http.MethodGet, route+"tokens", tokens)
}
{
add := panel.tokensAddRoute(ctx)
router.Handler(http.MethodGet, route+"tokens/add", add)
router.Handler(http.MethodPost, route+"tokens/add", add)
}
{
delete := panel.tokensDeleteRoute(ctx)
router.Handler(http.MethodPost, route+"tokens/delete", delete)
}
// ensure that the user is logged in!
return panel.Dependencies.Auth.Protect(router, scopes.ScopeUserLoggedIn, nil), nil
return panel.Dependencies.Auth.Protect(router, false, scopes.ScopeUserLoggedIn, nil), nil
}
type userFormContext struct {
@ -137,7 +158,7 @@ func (panel *UserPanel) UserFormContext(tpl *templating.Template[userFormContext
return func(ctx httpx.FormContext, r *http.Request) any {
uctx := userFormContext{FormContext: ctx}
if user, err := panel.Dependencies.Auth.UserOf(r); err == nil {
if user, err := panel.Dependencies.Auth.UserOfSession(r); err == nil {
uctx.User = &user.User
}
return tpl.Context(r, uctx, funcs...)

View file

@ -53,7 +53,7 @@ func (panel *UserPanel) routePassword(ctx context.Context) http.Handler {
return struct{}{}, errPasswordsNotIdentical
}
user, err := panel.Dependencies.Auth.UserOf(r)
user, err := panel.Dependencies.Auth.UserOfSession(r)
if err != nil {
return struct{}{}, err
}

View file

@ -57,7 +57,7 @@ func (panel *UserPanel) sshRoute(ctx context.Context) http.Handler {
)
return tpl.HTMLHandler(func(r *http.Request) (sc SSHTemplateContext, err error) {
user, err := panel.Dependencies.Auth.UserOf(r)
user, err := panel.Dependencies.Auth.UserOfSession(r)
if err != nil {
return sc, err
}
@ -89,6 +89,7 @@ var (
errInvalidUser = errors.New("invalid user")
errKeyParse = errors.New("unable to parse ssh key")
errAddKey = errors.New("unable to add key")
errAddToken = errors.New("unable to add token")
)
func (panel *UserPanel) sshDeleteRoute(ctx context.Context) http.Handler {
@ -99,7 +100,7 @@ func (panel *UserPanel) sshDeleteRoute(ctx context.Context) http.Handler {
httpx.HTMLInterceptor.Fallback.ServeHTTP(w, r)
return
}
user, err := panel.Dependencies.Auth.UserOf(r)
user, err := panel.Dependencies.Auth.UserOfSession(r)
if err != nil {
logger.Err(err).Str("action", "delete ssh key").Msg("failed to get current user")
httpx.HTMLInterceptor.Fallback.ServeHTTP(w, r)
@ -119,7 +120,7 @@ func (panel *UserPanel) sshDeleteRoute(ctx context.Context) http.Handler {
return
}
http.Redirect(w, r, "/user/ssh/", http.StatusSeeOther)
http.Redirect(w, r, string(menuSSH.Path), http.StatusSeeOther)
})
}
@ -158,7 +159,7 @@ func (panel *UserPanel) sshAddRoute(ctx context.Context) http.Handler {
RenderTemplateContext: templating.FormTemplateContext(tpl),
Validate: func(r *http.Request, values map[string]string) (ak addKeyResult, err error) {
ak.User, err = panel.Dependencies.Auth.UserOf(r)
ak.User, err = panel.Dependencies.Auth.UserOfSession(r)
if err != nil || ak.User == nil {
return ak, errInvalidUser
}

View file

@ -0,0 +1,56 @@
<div class="pure-u-1">
<p>
This page allows you to add, view and remove tokens from your distillery account.
</p>
</div>
<div class="pure-u-1">
<h2>My Tokens</h2>
<p>
This table shows tokens currently associated with your account.
Tokens can be used to access the API programatically.
</p>
<div class="padding">
<div class="overflow">
<table class="pure-table pure-table-bordered">
<thead>
<tr>
<th>
Token
</th>
<th>
Description
</th>
<th>
Actions
</th>
</tr>
</thead>
<tbody>
{{ $csrf := .CSRF }}
{{ range .Tokens }}
<tr>
<td>
<code class="copy">{{ .Token }}</code>
</td>
<td>
{{ .Description }}
</td>
<td>
<div class="pure-button-group" role="group">
<form action="/user/tokens/delete" method="POST" class="pure-form-group">
<input type="hidden" name="token" value="{{ .Token }}">
<input type="submit" class="pure-button pure-button-danger" value="Delete">
{{ $csrf }}
</form>
</div>
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
</div>
</div>

View file

@ -0,0 +1,9 @@
{{ template "form.html" . }}
{{ define "form/button" }}Add{{ end }}
{{ define "form/inside" }}
<div>
<p>
Use this form to add a new <em>Token</em> to your account.
</p>
</div>
{{ end }}

View file

@ -0,0 +1,153 @@
package panel
import (
"context"
"net/http"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/FAU-CDI/wisski-distillery/internal/models"
"github.com/rs/zerolog"
"github.com/tkw1536/pkglib/httpx"
"github.com/tkw1536/pkglib/httpx/field"
_ "embed"
)
//go:embed "templates/tokens.html"
var tokensHTML []byte
var tokensTemplate = templating.Parse[TokenTemplateContext](
"tokens.html", tokensHTML, nil,
templating.Title("Tokens"),
templating.Assets(assets.AssetsUser),
)
type TokenTemplateContext struct {
templating.RuntimeFlags
Tokens []models.Token
}
func (panel *UserPanel) tokensRoute(ctx context.Context) http.Handler {
tpl := tokensTemplate.Prepare(
panel.Dependencies.Templating,
templating.Crumbs(
menuUser,
menuTokens,
),
templating.Actions(
menuTokensAdd,
),
)
return tpl.HTMLHandler(func(r *http.Request) (tc TokenTemplateContext, err error) {
// list the user
user, err := panel.Dependencies.Auth.UserOfSession(r)
if err != nil || user == nil {
return tc, err
}
// get the tokens
tc.Tokens, err = panel.Dependencies.Tokens.Tokens(r.Context(), user.User.User)
return tc, err
})
}
func (panel *UserPanel) tokensDeleteRoute(ctx context.Context) http.Handler {
logger := zerolog.Ctx(ctx)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
logger.Err(err).Str("action", "delete token").Msg("failed to parse form")
httpx.HTMLInterceptor.Fallback.ServeHTTP(w, r)
return
}
user, err := panel.Dependencies.Auth.UserOfSession(r)
if err != nil {
logger.Err(err).Str("action", "delete token").Msg("failed to get current user")
httpx.HTMLInterceptor.Fallback.ServeHTTP(w, r)
return
}
token := r.PostFormValue("token")
if token == "" {
logger.Err(err).Str("action", "delete token").Msg("failed to get token")
httpx.HTMLInterceptor.Fallback.ServeHTTP(w, r)
return
}
if err := panel.Dependencies.Tokens.Remove(r.Context(), user.User.User, token); err != nil {
logger.Err(err).Str("action", "delete token").Msg("failed to delete token")
httpx.HTMLInterceptor.Fallback.ServeHTTP(w, r)
return
}
http.Redirect(w, r, string(menuTokens.Path), http.StatusSeeOther)
})
}
//go:embed "templates/tokens_add.html"
var tokensAddHTML []byte
var tokensAddTemplate = templating.ParseForm(
"tokens_add.html", tokensAddHTML, httpx.FormTemplate,
templating.Title("Add Token"),
templating.Assets(assets.AssetsUser),
)
type addTokenResult struct {
User *auth.AuthUser
Description string
Scopes []string
}
func (panel *UserPanel) tokensAddRoute(ctx context.Context) http.Handler {
tpl := tokensAddTemplate.Prepare(
panel.Dependencies.Templating,
templating.Crumbs(
menuUser,
menuTokens,
menuTokensAdd,
),
)
return &httpx.Form[addTokenResult]{
Fields: []field.Field{
{Name: "description", Type: field.Text, Label: "Description"},
},
FieldTemplate: field.PureCSSFieldTemplate,
RenderTemplate: tpl.Template(),
RenderTemplateContext: templating.FormTemplateContext(tpl),
Validate: func(r *http.Request, values map[string]string) (at addTokenResult, err error) {
at.User, err = panel.Dependencies.Auth.UserOfSession(r)
if err != nil || at.User == nil {
return at, errInvalidUser
}
at.Description = values["description"]
if at.Description == "" {
at.Description = "API Key"
}
at.Scopes = nil
return at, nil
},
RenderSuccess: func(at addTokenResult, values map[string]string, w http.ResponseWriter, r *http.Request) error {
// add the key to the user
_, err := panel.Dependencies.Tokens.Add(r.Context(), at.User.User.User, at.Description, at.Scopes)
if err != nil {
return err
}
if err != nil {
return errAddToken
}
// everything went fine, redirect the user back to the user page!
http.Redirect(w, r, string(menuTokens.Path), http.StatusSeeOther)
return nil
},
}
}

View file

@ -33,7 +33,7 @@ func (panel *UserPanel) routeTOTPEnable(ctx context.Context) http.Handler {
FieldTemplate: field.PureCSSFieldTemplate,
SkipForm: func(r *http.Request) (data struct{}, skip bool) {
user, err := panel.Dependencies.Auth.UserOf(r)
user, err := panel.Dependencies.Auth.UserOfSession(r)
return struct{}{}, err == nil && user != nil && user.IsTOTPEnabled()
},
@ -43,7 +43,7 @@ func (panel *UserPanel) routeTOTPEnable(ctx context.Context) http.Handler {
Validate: func(r *http.Request, values map[string]string) (struct{}, error) {
password := values["password"]
user, err := panel.Dependencies.Auth.UserOf(r)
user, err := panel.Dependencies.Auth.UserOfSession(r)
if err != nil {
return struct{}{}, err
}
@ -105,11 +105,11 @@ func (panel *UserPanel) routeTOTPEnroll(ctx context.Context) http.Handler {
FieldTemplate: field.PureCSSFieldTemplate,
SkipForm: func(r *http.Request) (data struct{}, skip bool) {
user, err := panel.Dependencies.Auth.UserOf(r)
user, err := panel.Dependencies.Auth.UserOfSession(r)
return struct{}{}, err == nil && user != nil && user.IsTOTPEnabled()
},
RenderTemplateContext: func(context httpx.FormContext, r *http.Request) any {
user, err := panel.Dependencies.Auth.UserOf(r)
user, err := panel.Dependencies.Auth.UserOfSession(r)
ctx := totpEnrollContext{
userFormContext: userFormContext{
@ -136,7 +136,7 @@ func (panel *UserPanel) routeTOTPEnroll(ctx context.Context) http.Handler {
Validate: func(r *http.Request, values map[string]string) (struct{}, error) {
password, otp := values["password"], values["otp"]
user, err := panel.Dependencies.Auth.UserOf(r)
user, err := panel.Dependencies.Auth.UserOfSession(r)
if err != nil {
return struct{}{}, err
}
@ -184,7 +184,7 @@ func (panel *UserPanel) routeTOTPDisable(ctx context.Context) http.Handler {
FieldTemplate: field.PureCSSFieldTemplate,
SkipForm: func(r *http.Request) (data struct{}, skip bool) {
user, err := panel.Dependencies.Auth.UserOf(r)
user, err := panel.Dependencies.Auth.UserOfSession(r)
return struct{}{}, err == nil && user != nil && !user.IsTOTPEnabled()
},
RenderTemplate: tpl.Template(),
@ -193,7 +193,7 @@ func (panel *UserPanel) routeTOTPDisable(ctx context.Context) http.Handler {
Validate: func(r *http.Request, values map[string]string) (struct{}, error) {
password, otp := values["password"], values["otp"]
user, err := panel.Dependencies.Auth.UserOf(r)
user, err := panel.Dependencies.Auth.UserOfSession(r)
if err != nil {
return struct{}{}, err
}

View file

@ -51,12 +51,13 @@ func (panel *UserPanel) routeUser(ctx context.Context) http.Handler {
menuChangePassword,
menuTOTPAction,
menuSSH,
menuTokens,
),
)
return tpl.HTMLHandlerWithFlags(func(r *http.Request) (uc userContext, funcs []templating.FlagFunc, err error) {
// find the user
uc.AuthUser, err = panel.Dependencies.Auth.UserOf(r)
uc.AuthUser, err = panel.Dependencies.Auth.UserOfSession(r)
if err != nil || uc.AuthUser == nil {
return uc, nil, err
}