WIPL tokens
This commit is contained in:
parent
c09c729157
commit
161e08fe1f
25 changed files with 716 additions and 63 deletions
1
API.md
1
API.md
|
|
@ -4,5 +4,6 @@ The distillery comes with an API served under `/api/`.
|
|||
It is still a work in progress, and will be polished and properly implemented at a later point.
|
||||
The API is currently disabled by default, and needs to be enabled in `distillery.yaml`.
|
||||
|
||||
- `/api/v1/auth`: Returns user information
|
||||
- `/api/v1/systems`: Returns a (publically visible) list of systems
|
||||
- `/api/v1/news`: Returns JSON containing all news items
|
||||
54
internal/dis/component/auth/api/api.go
Normal file
54
internal/dis/component/auth/api/api.go
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth"
|
||||
)
|
||||
|
||||
type API struct {
|
||||
component.Base
|
||||
Dependencies struct {
|
||||
Auth *auth.Auth
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
_ component.Routeable = (*API)(nil)
|
||||
)
|
||||
|
||||
func (api *API) Routes() component.Routes {
|
||||
return component.Routes{
|
||||
Prefix: "/api/v1/auth/",
|
||||
Exact: true,
|
||||
}
|
||||
}
|
||||
|
||||
type AuthInfo struct {
|
||||
// User returns the authenticated user.
|
||||
// If there is no user, contains the empty string.
|
||||
User string
|
||||
|
||||
// Token indicates if the user is authenticated with a token.
|
||||
Token bool
|
||||
}
|
||||
|
||||
func (a *API) HandleRoute(ctx context.Context, path string) (http.Handler, error) {
|
||||
return &Handler[AuthInfo]{
|
||||
Config: a.Config,
|
||||
Auth: a.Dependencies.Auth,
|
||||
|
||||
Methods: []string{"GET"},
|
||||
|
||||
Handler: func(s string, r *http.Request) (ai AuthInfo, err error) {
|
||||
var user *auth.AuthUser
|
||||
user, ai.Token, err = a.Dependencies.Auth.UserOf(r)
|
||||
if user != nil {
|
||||
ai.User = user.User.User
|
||||
}
|
||||
return
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -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/tokens"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/sql"
|
||||
"github.com/gorilla/sessions"
|
||||
|
|
@ -19,12 +20,17 @@ type Auth struct {
|
|||
UserDeleteHooks []component.UserDeleteHook
|
||||
Templating *templating.Templating
|
||||
ScopeProviders []component.ScopeProvider
|
||||
Tokens *tokens.Tokens
|
||||
}
|
||||
|
||||
store lazy.Lazy[sessions.Store]
|
||||
|
||||
scopeInfos lazy.Lazy[[]component.ScopeInfo]
|
||||
scopeIndex lazy.Lazy[map[component.Scope]int]
|
||||
scopeMap lazy.Lazy[map[component.Scope]scopeMapEntry]
|
||||
}
|
||||
|
||||
type scopeMapEntry struct {
|
||||
Provider component.ScopeProvider
|
||||
Info component.ScopeInfo
|
||||
}
|
||||
|
||||
var (
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ var (
|
|||
func (next *Next) Routes() component.Routes {
|
||||
return component.Routes{
|
||||
Prefix: "/next/",
|
||||
Decorator: next.Dependencies.Auth.Require(scopes.ScopeUserLoggedIn, nil),
|
||||
Decorator: next.Dependencies.Auth.Require(true, scopes.ScopeUserLoggedIn, nil),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -80,7 +80,7 @@ func (next *Next) HandleRoute(ctx context.Context, path string) (http.Handler, e
|
|||
}
|
||||
|
||||
// get the user
|
||||
user, err := next.Dependencies.Auth.UserOf(r)
|
||||
user, _, err := next.Dependencies.Auth.UserOf(r)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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...)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
56
internal/dis/component/auth/panel/templates/tokens.html
Normal file
56
internal/dis/component/auth/panel/templates/tokens.html
Normal 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>
|
||||
|
||||
|
|
@ -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 }}
|
||||
153
internal/dis/component/auth/panel/tokens.go
Normal file
153
internal/dis/component/auth/panel/tokens.go
Normal 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
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,22 +9,30 @@ import (
|
|||
"github.com/tkw1536/pkglib/httpx"
|
||||
)
|
||||
|
||||
// Protect returns a new handler which requires a user to be logged in and have the provided scope and
|
||||
// Protect returns a new handler which requires a user to be logged in and have the provided scope.
|
||||
//
|
||||
// AllowToken determines if a token is allowed instead of a user session.
|
||||
//
|
||||
// If an unauthenticated user attempts to access the returned handler, they are redirected to the login endpoint.
|
||||
// If an authenticated user is missing the given scope, 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, scope component.Scope, param func(*http.Request) string) http.Handler {
|
||||
func (auth *Auth) Protect(handler http.Handler, AllowToken bool, scope component.Scope, param func(*http.Request) string) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var forbiddenMessage string
|
||||
var paramValue string
|
||||
|
||||
// load the user in the session
|
||||
user, err := auth.UserOf(r)
|
||||
// TODO<tokens>: Check if API access is allowed
|
||||
user, token, err := auth.UserOf(r)
|
||||
if err != nil {
|
||||
goto err
|
||||
}
|
||||
|
||||
// token was set, but not allowed!
|
||||
if token && !AllowToken {
|
||||
goto forbidden
|
||||
}
|
||||
|
||||
// if there is no user in the session, they need to login first!
|
||||
if user == nil {
|
||||
// we can't redirect anything other than GET
|
||||
|
|
@ -75,9 +83,10 @@ func (auth *Auth) Protect(handler http.Handler, scope component.Scope, param fun
|
|||
})
|
||||
}
|
||||
|
||||
// Require returns a slice containing one decorator that acts like Protect(scope,param) on every request.
|
||||
func (auth *Auth) Require(scope component.Scope, param func(*http.Request) string) func(http.Handler) http.Handler {
|
||||
// Require returns a slice containing one decorator that acts like auth.Protect(allowToken,scope,param) on every request.
|
||||
func (auth *Auth) Require(allowToken bool, scope component.Scope, param func(*http.Request) string) func(http.Handler) http.Handler {
|
||||
// TODO: Work on this stuff
|
||||
return func(h http.Handler) http.Handler {
|
||||
return auth.Protect(h, scope, param)
|
||||
return auth.Protect(h, allowToken, scope, param)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,46 +13,62 @@ var (
|
|||
ErrNoParam = errors.New("scope does not take parameter")
|
||||
)
|
||||
|
||||
// CheckScope checks if the given session has the given scope.
|
||||
// If the user is denied a scope, the error will be of type AccessDeniedError.
|
||||
func (auth *Auth) CheckScope(param string, scope component.Scope, r *http.Request) error {
|
||||
// get all the infos about all of the scopes
|
||||
infos := auth.scopeInfos.Get(func() []component.ScopeInfo {
|
||||
infos := make([]component.ScopeInfo, len(auth.Dependencies.ScopeProviders))
|
||||
for i, p := range auth.Dependencies.ScopeProviders {
|
||||
infos[i] = p.Scope()
|
||||
// Scopes returns a map of all available scopes
|
||||
func (auth *Auth) Scopes() map[component.Scope]component.ScopeInfo {
|
||||
scopes := auth.getScopeMap()
|
||||
mp := make(map[component.Scope]component.ScopeInfo, len(scopes))
|
||||
for scope, entry := range scopes {
|
||||
mp[scope] = entry.Info
|
||||
}
|
||||
return mp
|
||||
}
|
||||
|
||||
// getScopeMap return a (cached version of) all scopes
|
||||
func (auth *Auth) getScopeMap() map[component.Scope]scopeMapEntry {
|
||||
return auth.scopeMap.Get(func() map[component.Scope]scopeMapEntry {
|
||||
mp := make(map[component.Scope]scopeMapEntry, len(auth.Dependencies.ScopeProviders))
|
||||
for _, p := range auth.Dependencies.ScopeProviders {
|
||||
info := p.Scope()
|
||||
mp[info.Scope] = scopeMapEntry{
|
||||
Provider: p,
|
||||
Info: info,
|
||||
}
|
||||
}
|
||||
return infos
|
||||
return mp
|
||||
})
|
||||
}
|
||||
|
||||
// find where in teh list of parameters it is!
|
||||
index, ok := auth.scopeIndex.Get(func() map[component.Scope]int {
|
||||
m := make(map[component.Scope]int, len(infos))
|
||||
for idx, i := range infos {
|
||||
m[i.Scope] = idx
|
||||
}
|
||||
return m
|
||||
})[scope]
|
||||
// CheckScope checks if the given request is associated with the given request.
|
||||
// A request can be one of two types:
|
||||
// - A signed in user with an implicitly associated set of scopes
|
||||
// - A session authorized with a token only
|
||||
// If the request is denied a scope, the error will be of type AccessDeniedError.
|
||||
func (auth *Auth) CheckScope(param string, scope component.Scope, r *http.Request) error {
|
||||
// the empty scope is always permitted implicitly
|
||||
if scope == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
entry, ok := auth.getScopeMap()[scope]
|
||||
if !ok {
|
||||
return ErrUnknownScope
|
||||
}
|
||||
|
||||
// check that we take a parameter
|
||||
if infos[index].TakesParam && param == "" {
|
||||
if entry.Info.TakesParam && param == "" {
|
||||
return ErrParamRequired
|
||||
}
|
||||
if !infos[index].TakesParam && param != "" {
|
||||
if !entry.Info.TakesParam && param != "" {
|
||||
return ErrNoParam
|
||||
}
|
||||
|
||||
// call the checker and return an error
|
||||
ok, err := auth.Dependencies.ScopeProviders[index].HasScope(param, r)
|
||||
ok, err := entry.Provider.HasScope(param, r)
|
||||
if err != nil {
|
||||
return infos[index].CheckError(err)
|
||||
return entry.Info.CheckError(err)
|
||||
}
|
||||
if ok {
|
||||
return nil
|
||||
}
|
||||
return infos[index].DeniedError()
|
||||
return entry.Info.DeniedError()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,6 @@ func (*AdminLoggedIn) Scope() component.ScopeInfo {
|
|||
}
|
||||
|
||||
func (al *AdminLoggedIn) HasScope(param string, r *http.Request) (bool, error) {
|
||||
user, err := al.Dependencies.Auth.UserOf(r)
|
||||
user, _, err := al.Dependencies.Auth.UserOf(r)
|
||||
return user != nil && user.IsAdmin() && user.IsTOTPEnabled(), err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,6 @@ func (*UserLoggedIn) Scope() component.ScopeInfo {
|
|||
}
|
||||
|
||||
func (iu *UserLoggedIn) HasScope(param string, r *http.Request) (bool, error) {
|
||||
user, err := iu.Dependencies.Auth.UserOf(r)
|
||||
user, _, err := iu.Dependencies.Auth.UserOf(r)
|
||||
return user != nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,12 +18,42 @@ import (
|
|||
_ "embed"
|
||||
)
|
||||
|
||||
// UserOf returns the user logged into the given request.
|
||||
// If there is no user associated with the given user, user and error will be nil.
|
||||
// UserOf returns the user logged into the provided request.
|
||||
// token indicates if the user used a token to authenticate, or a browser session was used.
|
||||
// A token takes priority over a user in a session.
|
||||
//
|
||||
// If there is no user associated with the given request, user and error are nil, and token is false.
|
||||
// An invalid session, expired token, or disabled user all result in user = nil.
|
||||
//
|
||||
// When no UserOf exists in the given session returns nil.
|
||||
// An invalid session (for a UserOf)
|
||||
func (auth *Auth) UserOf(r *http.Request) (user *AuthUser, err error) {
|
||||
func (auth *Auth) UserOf(r *http.Request) (user *AuthUser, token bool, err error) {
|
||||
// check the user from the token first
|
||||
{
|
||||
user, err := auth.UserOfToken(r)
|
||||
if user != nil && err == nil {
|
||||
return user, true, nil
|
||||
}
|
||||
}
|
||||
|
||||
// fallback to using session
|
||||
{
|
||||
user, err := auth.UserOfSession(r)
|
||||
return user, false, err
|
||||
}
|
||||
}
|
||||
|
||||
// UserOfToken returns the user associated with the token in request.
|
||||
func (auth *Auth) UserOfToken(r *http.Request) (user *AuthUser, err error) {
|
||||
// get the token object
|
||||
token, err := auth.Dependencies.Tokens.TokenOf(r)
|
||||
if token == nil {
|
||||
return nil, err
|
||||
}
|
||||
return auth.checkUser(r.Context(), token.User)
|
||||
}
|
||||
|
||||
// UserOfSession returns the user of the session associated with r.
|
||||
func (auth *Auth) UserOfSession(r *http.Request) (user *AuthUser, err error) {
|
||||
ctx := r.Context()
|
||||
if user, ok := ctx.Value(ctxUserKey).(*AuthUser); ok && user != nil {
|
||||
return user, nil
|
||||
|
|
@ -44,9 +74,12 @@ func (auth *Auth) UserOf(r *http.Request) (user *AuthUser, err error) {
|
|||
if !ok || nameS == "" {
|
||||
return nil, nil
|
||||
}
|
||||
return auth.checkUser(ctx, nameS)
|
||||
}
|
||||
|
||||
func (auth *Auth) checkUser(ctx context.Context, name string) (user *AuthUser, err error) {
|
||||
// fetch the user, check if they still exist
|
||||
user, err = auth.User(ctx, nameS)
|
||||
user, err = auth.User(ctx, name)
|
||||
if err == ErrUserNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
|
|
@ -55,7 +88,7 @@ func (auth *Auth) UserOf(r *http.Request) (user *AuthUser, err error) {
|
|||
}
|
||||
|
||||
// user isn't enabled
|
||||
if !user.IsEnabled() {
|
||||
if user == nil || !user.IsEnabled() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
|
@ -73,7 +106,7 @@ func (auth *Auth) session(r *http.Request) (*sessions.Session, error) {
|
|||
|
||||
func (auth *Auth) Menu(r *http.Request) []component.MenuItem {
|
||||
|
||||
user, err := auth.UserOf(r)
|
||||
user, err := auth.UserOfSession(r)
|
||||
if user == nil || err != nil {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -176,7 +209,7 @@ func (auth *Auth) authLogin(ctx context.Context) http.Handler {
|
|||
},
|
||||
|
||||
SkipForm: func(r *http.Request) (user *AuthUser, skip bool) {
|
||||
user, err := auth.UserOf(r)
|
||||
user, err := auth.UserOfSession(r)
|
||||
return user, err == nil && user != nil
|
||||
},
|
||||
|
||||
|
|
|
|||
90
internal/dis/component/auth/tokens/check.go
Normal file
90
internal/dis/component/auth/tokens/check.go
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
package tokens
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/models"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
const (
|
||||
authHeader = "Authorization" // authorization
|
||||
authBearer = "Bearer" + " " // Prefix for bearer
|
||||
)
|
||||
|
||||
// TokenOf returns the token header found in the given request.
|
||||
// If r is nil, or there is no token, returns nil.
|
||||
// Error is only set if there is an error accessing the table that stores tokens.
|
||||
func (tok *Tokens) TokenOf(r *http.Request) (*models.Token, error) {
|
||||
if r == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// make sure that the authorization header exists and starts with the bearer
|
||||
auth := r.Header.Get(authHeader)
|
||||
if !strings.HasPrefix(auth, authBearer) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// get the token
|
||||
id := strings.TrimSpace(strings.TrimPrefix(auth, authBearer))
|
||||
if id == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
table, err := tok.table(r.Context())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// take a single object from the tokens
|
||||
var tokenObj models.Token
|
||||
res := table.Where(&models.Token{Token: id}).Find(&tokenObj)
|
||||
|
||||
if res.Error != nil {
|
||||
return nil, errors.Join(ErrNoToken, res.Error)
|
||||
}
|
||||
if res.RowsAffected == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// and return the token object
|
||||
return &tokenObj, nil
|
||||
}
|
||||
|
||||
var ErrNoToken = errors.New("no token")
|
||||
|
||||
// Check checks if there is a token in the given request and if this request has an appropriate token with the appropriate scope.
|
||||
//
|
||||
// If the token is found and has the requested token, returns true, nil.
|
||||
// If there is a token found, but the specific scope is not set, returns false, nil.
|
||||
// If there is no valid authentication token found, returns false and an error that wraps ErrNoToken.
|
||||
// In other cases, other errors may be returned.
|
||||
//
|
||||
// Note that the scope may require an parameter to be validated.
|
||||
// This validation should take place in the appropriate ScopeProvider; which should recursively invoke this method.
|
||||
func (tok *Tokens) Check(r *http.Request, scope component.Scope) (bool, error) {
|
||||
// get the token object from the request
|
||||
tokenObj, err := tok.TokenOf(r)
|
||||
if tokenObj == nil {
|
||||
if err == nil {
|
||||
return false, ErrNoToken
|
||||
}
|
||||
return false, errors.Join(ErrNoToken, err)
|
||||
}
|
||||
|
||||
// TODO: Do we need this function?
|
||||
|
||||
// get the scopes
|
||||
scopes := tokenObj.GetScopes()
|
||||
if scopes == nil {
|
||||
// all scopes (implicitly)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// else check if they are contained
|
||||
return slices.Contains(scopes, string(scope)), nil
|
||||
}
|
||||
148
internal/dis/component/auth/tokens/tokens.go
Normal file
148
internal/dis/component/auth/tokens/tokens.go
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
package tokens
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"strings"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/sql"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/models"
|
||||
"github.com/tkw1536/pkglib/password"
|
||||
"github.com/tkw1536/pkglib/reflectx"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Tokens implements Tokens
|
||||
type Tokens struct {
|
||||
component.Base
|
||||
|
||||
Dependencies struct {
|
||||
SQL *sql.SQL
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
_ component.UserDeleteHook = (*Tokens)(nil)
|
||||
_ component.Table = (*Tokens)(nil)
|
||||
)
|
||||
|
||||
func (tok *Tokens) TableInfo() component.TableInfo {
|
||||
return component.TableInfo{
|
||||
Name: models.TokensTable,
|
||||
Model: reflectx.MakeType[models.Token](),
|
||||
}
|
||||
}
|
||||
|
||||
func (tok *Tokens) table(ctx context.Context) (*gorm.DB, error) {
|
||||
return tok.Dependencies.SQL.QueryTable(ctx, tok)
|
||||
}
|
||||
|
||||
func (tok *Tokens) OnUserDelete(ctx context.Context, user *models.User) error {
|
||||
table, err := tok.table(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return table.Delete(&models.Token{}, &models.Token{User: user.User}).Error
|
||||
}
|
||||
|
||||
// Tokens returns a list of tokens for the given user
|
||||
func (tok *Tokens) Tokens(ctx context.Context, user string) ([]models.Token, error) {
|
||||
// the empty user has no tokens
|
||||
if user == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// get the table
|
||||
table, err := tok.table(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var tokens []models.Token
|
||||
|
||||
// make a query to find all keys (in the underlying model)
|
||||
query := table.Find(&tokens, &models.Token{User: user})
|
||||
if query.Error != nil {
|
||||
return nil, query.Error
|
||||
}
|
||||
|
||||
return tokens, nil
|
||||
}
|
||||
|
||||
const (
|
||||
tokenGroupLength = 8
|
||||
tokenGroupCount = 8
|
||||
tokenSeparator = "-"
|
||||
tokenCharset password.Charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
)
|
||||
|
||||
// NewToken generates a new token
|
||||
func NewToken() (string, error) {
|
||||
// generate a new password
|
||||
token, err := password.Generate(rand.Reader, tokenGroupCount*tokenGroupLength, tokenCharset)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// insert the token group separators
|
||||
var result strings.Builder
|
||||
result.Grow(len(token) + (tokenGroupCount-1)*len(tokenSeparator))
|
||||
|
||||
for i := 0; i < tokenGroupCount; i++ {
|
||||
if i != 0 {
|
||||
result.WriteString(tokenSeparator)
|
||||
}
|
||||
|
||||
start := i * tokenGroupLength
|
||||
result.WriteString(token[start : start+tokenGroupLength])
|
||||
}
|
||||
|
||||
return result.String(), nil
|
||||
|
||||
}
|
||||
|
||||
// Add adds a new token, unless it already exists.
|
||||
// The token is granted scopes with .SetScopes(scopes).
|
||||
func (tok *Tokens) Add(ctx context.Context, user string, description string, scopes []string) (*models.Token, error) {
|
||||
|
||||
// create a new token and set the scopes
|
||||
mk := models.Token{
|
||||
User: user,
|
||||
Description: description,
|
||||
}
|
||||
mk.SetScopes(scopes)
|
||||
|
||||
// generate a new random password
|
||||
var err error
|
||||
mk.Token, err = NewToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// get the table
|
||||
table, err := tok.table(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// create the token instance
|
||||
if err := table.Create(&mk).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// and return
|
||||
return &mk, nil
|
||||
}
|
||||
|
||||
// Remove removes a token with the given token from the user
|
||||
func (tok *Tokens) Remove(ctx context.Context, user, token string) error {
|
||||
// get the table
|
||||
table, err := tok.table(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// and do the delete
|
||||
return table.Where("user = ? AND token = ?", user, token).Delete(&models.Token{}).Error
|
||||
}
|
||||
|
|
@ -25,7 +25,7 @@ var ErrUserNotFound = errors.New("user not found")
|
|||
func (auth *Auth) TableInfo() component.TableInfo {
|
||||
return component.TableInfo{
|
||||
Name: models.UserTable,
|
||||
Model: reflectx.TypeOf[models.User](),
|
||||
Model: reflectx.MakeType[models.User](),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ func (admin *Admin) Routes() component.Routes {
|
|||
return component.Routes{
|
||||
Prefix: "/admin/",
|
||||
CSRF: true,
|
||||
Decorator: admin.Dependencies.Auth.Require(scopes.ScopeAdminLoggedIn, nil),
|
||||
Decorator: admin.Dependencies.Auth.Require(false, scopes.ScopeAdminLoggedIn, nil),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -158,7 +158,7 @@ func (admin *Admin) useraction(ctx context.Context, name string, action func(r *
|
|||
return
|
||||
}
|
||||
|
||||
me, err := admin.Dependencies.Auth.UserOf(r)
|
||||
me, err := admin.Dependencies.Auth.UserOfSession(r)
|
||||
if err != nil {
|
||||
logger.Err(err).Str("action", name).Msg("failed to get current user")
|
||||
httpx.HTMLInterceptor.Fallback.ServeHTTP(w, r)
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ func (li *ListInstances) ShouldShowList(r *http.Request) bool {
|
|||
return allowPrivate
|
||||
}
|
||||
|
||||
user, _ := li.Dependencies.Auth.UserOf(r)
|
||||
user, _, _ := li.Dependencies.Auth.UserOf(r)
|
||||
if user == nil {
|
||||
return allowPublic
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -8,10 +8,12 @@ import (
|
|||
|
||||
"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/auth/api"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/next"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/panel"
|
||||
"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/binder"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/docker"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/exporter"
|
||||
|
|
@ -145,6 +147,7 @@ func (dis *Distillery) allComponents() []initFunc {
|
|||
auto[*policy.Policy],
|
||||
auto[*panel.UserPanel],
|
||||
auto[*next.Next],
|
||||
auto[*tokens.Tokens],
|
||||
|
||||
//scopes
|
||||
auto[*scopes.UserLoggedIn],
|
||||
|
|
@ -196,6 +199,8 @@ func (dis *Distillery) allComponents() []initFunc {
|
|||
auto[*cron.Cron],
|
||||
|
||||
// API
|
||||
auto[*api.API],
|
||||
auto[*list.API],
|
||||
auto[*list.API],
|
||||
auto[*news.API],
|
||||
}
|
||||
|
|
|
|||
50
internal/models/token.go
Normal file
50
internal/models/token.go
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// TokensTable is the name of the table the 'Token' model is stored in.
|
||||
const TokensTable = "tokens"
|
||||
|
||||
// Token represents an access token for a specific user
|
||||
type Token struct {
|
||||
Pk uint `gorm:"column:pk;primaryKey"`
|
||||
|
||||
Token string `gorm:"column:token;unique:true;not null"`
|
||||
User string `gorm:"column:user;not null"` // (distillery) username
|
||||
|
||||
Description string `gorm:"column:description"`
|
||||
|
||||
AllScopes bool `gorm:"column:all;not null"`
|
||||
Scopes []byte `gorm:"column:scopes;not null"` // comma-seperated list of scopes
|
||||
}
|
||||
|
||||
// GetScopes returns the scopes associated with this Token.
|
||||
//
|
||||
// If this token implicitly has all scopes, returns nil.
|
||||
// If this token has no scopes, returns an empty string slice.
|
||||
func (token *Token) GetScopes() (scopes []string) {
|
||||
// all scopes
|
||||
if token.AllScopes {
|
||||
return nil
|
||||
}
|
||||
|
||||
// unmarshal the scopes associated with this token
|
||||
// and ensure that it is never nil.
|
||||
err := json.Unmarshal(token.Scopes, &scopes)
|
||||
if scopes == nil || err != nil {
|
||||
scopes = []string{}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// SetScopes sets the scopes associated to this token to scopes.
|
||||
// It scopes is nil, sets the token to permit all scopes.
|
||||
func (token *Token) SetScopes(scopes []string) {
|
||||
token.AllScopes = scopes == nil
|
||||
if token.AllScopes {
|
||||
scopes = []string{}
|
||||
}
|
||||
token.Scopes, _ = json.Marshal(scopes)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue