Implement scopes
This commit is contained in:
parent
be07ac5d83
commit
064ae2f564
15 changed files with 232 additions and 137 deletions
2
go.mod
2
go.mod
|
|
@ -16,7 +16,7 @@ require (
|
||||||
github.com/pquerna/otp v1.4.0
|
github.com/pquerna/otp v1.4.0
|
||||||
github.com/rs/zerolog v1.29.1
|
github.com/rs/zerolog v1.29.1
|
||||||
github.com/tkw1536/goprogram v0.3.5
|
github.com/tkw1536/goprogram v0.3.5
|
||||||
github.com/tkw1536/pkglib v0.0.0-20230501182828-8cb15f4fccd8
|
github.com/tkw1536/pkglib v0.0.0-20230503121722-b0c615fc34ee
|
||||||
github.com/yuin/goldmark v1.5.4
|
github.com/yuin/goldmark v1.5.4
|
||||||
github.com/yuin/goldmark-meta v1.1.0
|
github.com/yuin/goldmark-meta v1.1.0
|
||||||
golang.org/x/crypto v0.8.0
|
golang.org/x/crypto v0.8.0
|
||||||
|
|
|
||||||
4
go.sum
4
go.sum
|
|
@ -114,8 +114,8 @@ github.com/tdewolff/parse v2.3.4+incompatible/go.mod h1:8oBwCsVmUkgHO8M5iCzSIDtp
|
||||||
github.com/tdewolff/test v1.0.7 h1:8Vs0142DmPFW/bQeHRP3MV19m1gvndjUb1sn8yy74LM=
|
github.com/tdewolff/test v1.0.7 h1:8Vs0142DmPFW/bQeHRP3MV19m1gvndjUb1sn8yy74LM=
|
||||||
github.com/tkw1536/goprogram v0.3.5 h1:S0axKo3R/vGa4zhYqYDKAZEPhAfwUSSeMtVwnAu4sNY=
|
github.com/tkw1536/goprogram v0.3.5 h1:S0axKo3R/vGa4zhYqYDKAZEPhAfwUSSeMtVwnAu4sNY=
|
||||||
github.com/tkw1536/goprogram v0.3.5/go.mod h1:pYr4dMHOSVurbPQ4KTR0ett8XWNISbsRS6zlh9Nsxa8=
|
github.com/tkw1536/goprogram v0.3.5/go.mod h1:pYr4dMHOSVurbPQ4KTR0ett8XWNISbsRS6zlh9Nsxa8=
|
||||||
github.com/tkw1536/pkglib v0.0.0-20230501182828-8cb15f4fccd8 h1:adL2kWKOiDeSaaZi93sII5szzpowRSz2UrCNaAv760Y=
|
github.com/tkw1536/pkglib v0.0.0-20230503121722-b0c615fc34ee h1:MJZzhO8Fq7WVNGjWLhS4cGhWWmK92BMgzDtLSaYeFx4=
|
||||||
github.com/tkw1536/pkglib v0.0.0-20230501182828-8cb15f4fccd8/go.mod h1:0A1B9Cc5+yJXR3eeB14CqD4dFSbEjjWRo5Pr9M3XYuI=
|
github.com/tkw1536/pkglib v0.0.0-20230503121722-b0c615fc34ee/go.mod h1:0A1B9Cc5+yJXR3eeB14CqD4dFSbEjjWRo5Pr9M3XYuI=
|
||||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
|
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
|
||||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ func (dis *Distillery) init() {
|
||||||
lifetime.RegisterGroup[component.UserDeleteHook](&dis.lifetime)
|
lifetime.RegisterGroup[component.UserDeleteHook](&dis.lifetime)
|
||||||
lifetime.RegisterGroup[component.Table](&dis.lifetime)
|
lifetime.RegisterGroup[component.Table](&dis.lifetime)
|
||||||
lifetime.RegisterGroup[component.Menuable](&dis.lifetime)
|
lifetime.RegisterGroup[component.Menuable](&dis.lifetime)
|
||||||
|
lifetime.RegisterGroup[component.ScopeProvider](&dis.lifetime)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,13 @@ type Auth struct {
|
||||||
SQL *sql.SQL
|
SQL *sql.SQL
|
||||||
UserDeleteHooks []component.UserDeleteHook
|
UserDeleteHooks []component.UserDeleteHook
|
||||||
Templating *templating.Templating
|
Templating *templating.Templating
|
||||||
|
ScopeProviders []component.ScopeProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
store lazy.Lazy[sessions.Store]
|
store lazy.Lazy[sessions.Store]
|
||||||
|
|
||||||
|
scopeInfos lazy.Lazy[[]component.ScopeInfo]
|
||||||
|
scopeIndex lazy.Lazy[map[component.Scope]int]
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ var (
|
||||||
func (next *Next) Routes() component.Routes {
|
func (next *Next) Routes() component.Routes {
|
||||||
return component.Routes{
|
return component.Routes{
|
||||||
Prefix: "/next/",
|
Prefix: "/next/",
|
||||||
Decorator: next.Dependencies.Auth.Require(auth.User),
|
Decorator: next.Dependencies.Auth.Require(component.ScopeUserLoggedIn, nil),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ func (panel *UserPanel) Routes() component.Routes {
|
||||||
return component.Routes{
|
return component.Routes{
|
||||||
Prefix: "/user/",
|
Prefix: "/user/",
|
||||||
CSRF: true,
|
CSRF: true,
|
||||||
Decorator: panel.Dependencies.Auth.Require(nil),
|
Decorator: panel.Dependencies.Auth.Require(component.ScopeUserLoggedIn, nil),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -115,7 +115,7 @@ func (panel *UserPanel) HandleRoute(ctx context.Context, route string) (http.Han
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensure that the user is logged in!
|
// ensure that the user is logged in!
|
||||||
return panel.Dependencies.Auth.Protect(router, nil), nil
|
return panel.Dependencies.Auth.Protect(router, component.ScopeUserLoggedIn, nil), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type userFormContext struct {
|
type userFormContext struct {
|
||||||
|
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
package auth
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Permission represents a permission granted to a user.
|
|
||||||
//
|
|
||||||
// The nil permission represents any authenticated user.
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AllPermissions returns a new permission that checks if all the given permissions are set
|
|
||||||
func AllPermissions(clauses ...Permission) Permission {
|
|
||||||
return func(user *AuthUser, r *http.Request) (ok Grant, err error) {
|
|
||||||
for _, clause := range clauses {
|
|
||||||
perm, err := clause.Permit(user, r)
|
|
||||||
if err != nil {
|
|
||||||
return perm, err
|
|
||||||
}
|
|
||||||
if !perm.Granted() {
|
|
||||||
return perm, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// everything was fine
|
|
||||||
return grantAllow{}, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
@ -5,17 +5,19 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
|
||||||
"github.com/tkw1536/pkglib/httpx"
|
"github.com/tkw1536/pkglib/httpx"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Protect returns a new handler which requires a user to be logged in and pass the perm function.
|
// Protect returns a new handler which requires a user to be logged in and have the provided scope and
|
||||||
//
|
//
|
||||||
// If an unauthenticated user attempts to access the returned handler, they are redirected to the login endpoint.
|
// If an unauthenticated user attempts to access the returned handler, they are redirected to the login endpoint.
|
||||||
// If an authenticated user is missing permissions, a Forbidden response is called.
|
// If an authenticated 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.
|
// 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 {
|
func (auth *Auth) Protect(handler http.Handler, scope component.Scope, param func(*http.Request) string) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
var grant Grant
|
var forbiddenMessage string
|
||||||
|
var paramValue string
|
||||||
|
|
||||||
// load the user in the session
|
// load the user in the session
|
||||||
user, err := auth.UserOf(r)
|
user, err := auth.UserOf(r)
|
||||||
|
|
@ -38,16 +40,21 @@ func (auth *Auth) Protect(handler http.Handler, perm Permission) http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check if we need to load the parameter
|
||||||
|
if param != nil {
|
||||||
|
paramValue = param(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if we have the actual scope
|
||||||
{
|
{
|
||||||
var err error
|
err = auth.CheckScope(paramValue, scope, r)
|
||||||
// call the permission check
|
if ade, ok := err.(component.AccessDeniedError); ok {
|
||||||
grant, err = perm.Permit(user, r)
|
forbiddenMessage = ade.Error()
|
||||||
|
goto forbidden
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
goto err
|
goto err
|
||||||
}
|
}
|
||||||
if !grant.Granted() {
|
|
||||||
goto forbidden
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// store the user into the session, and then return the new session
|
// store the user into the session, and then return the new session
|
||||||
|
|
@ -56,14 +63,10 @@ func (auth *Auth) Protect(handler http.Handler, perm Permission) http.Handler {
|
||||||
return
|
return
|
||||||
forbidden:
|
forbidden:
|
||||||
{
|
{
|
||||||
message := "Forbidden"
|
|
||||||
if grant != nil {
|
|
||||||
message = grant.Denied()
|
|
||||||
}
|
|
||||||
httpx.Response{
|
httpx.Response{
|
||||||
ContentType: "text/plain",
|
ContentType: "text/plain",
|
||||||
StatusCode: http.StatusForbidden,
|
StatusCode: http.StatusForbidden,
|
||||||
Body: []byte(message),
|
Body: []byte(forbiddenMessage),
|
||||||
}.ServeHTTP(w, r)
|
}.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -72,31 +75,9 @@ func (auth *Auth) Protect(handler http.Handler, perm Permission) http.Handler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Require returns a slice containing one decorator that acts like Protect(perm) on every request.
|
// Require returns a slice containing one decorator that acts like Protect(scope,param) on every request.
|
||||||
// It returns
|
func (auth *Auth) Require(scope component.Scope, param func(*http.Request) string) func(http.Handler) http.Handler {
|
||||||
func (auth *Auth) Require(perm Permission) func(http.Handler) http.Handler {
|
|
||||||
return func(h http.Handler) http.Handler {
|
return func(h http.Handler) http.Handler {
|
||||||
return auth.Protect(h, perm)
|
return auth.Protect(h, scope, param)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Has checks if the given request has the given permission.
|
|
||||||
// If an error occurs, returns false.
|
|
||||||
func (auth *Auth) Has(perm Permission, r *http.Request) bool {
|
|
||||||
user, err := auth.UserOf(r)
|
|
||||||
if err != nil || user == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
ok, err := perm.Permit(user, r)
|
|
||||||
return err == nil && ok.Granted()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 Grant, err error) {
|
|
||||||
return Bool2Grant(user != nil && user.IsAdmin() && user.IsTOTPEnabled(), "user needs to have admin permissions and passcode enabled"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// User represents a permission that checks if a user is enabled
|
|
||||||
var User Permission = func(user *AuthUser, r *http.Request) (ok Grant, err error) {
|
|
||||||
return Bool2Grant(user != nil && user.IsEnabled(), "user needs to be enabled"), nil
|
|
||||||
}
|
|
||||||
|
|
|
||||||
58
internal/dis/component/auth/scope.go
Normal file
58
internal/dis/component/auth/scope.go
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrUnknownScope = errors.New("unknown scope")
|
||||||
|
ErrParamRequired = errors.New("scope requires parameter")
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
return infos
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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]
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return ErrUnknownScope
|
||||||
|
}
|
||||||
|
|
||||||
|
// check that we take a parameter
|
||||||
|
if infos[index].TakesParam && param == "" {
|
||||||
|
return ErrParamRequired
|
||||||
|
}
|
||||||
|
if !infos[index].TakesParam && param != "" {
|
||||||
|
return ErrNoParam
|
||||||
|
}
|
||||||
|
|
||||||
|
// call the checker and return an error
|
||||||
|
ok, err := auth.Dependencies.ScopeProviders[index].HasScope(param, r)
|
||||||
|
if err != nil {
|
||||||
|
return infos[index].CheckError(err)
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return infos[index].DeniedError()
|
||||||
|
}
|
||||||
33
internal/dis/component/auth/scopes/admin.go
Normal file
33
internal/dis/component/auth/scopes/admin.go
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
package scopes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AdminLoggedIn struct {
|
||||||
|
component.Base
|
||||||
|
Dependencies struct {
|
||||||
|
Auth *auth.Auth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ component.ScopeProvider = (*UserLoggedIn)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
func (*AdminLoggedIn) Scope() component.ScopeInfo {
|
||||||
|
return component.ScopeInfo{
|
||||||
|
Scope: component.ScopeAdminLoggedIn,
|
||||||
|
Description: "session has a signed in admin",
|
||||||
|
DeniedMessage: "user must be signed into an admin account with TOTP enabled",
|
||||||
|
TakesParam: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (al *AdminLoggedIn) HasScope(param string, r *http.Request) (bool, error) {
|
||||||
|
user, err := al.Dependencies.Auth.UserOf(r)
|
||||||
|
return user != nil && user.IsAdmin() && user.IsTOTPEnabled(), err
|
||||||
|
}
|
||||||
32
internal/dis/component/auth/scopes/user.go
Normal file
32
internal/dis/component/auth/scopes/user.go
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
package scopes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserLoggedIn struct {
|
||||||
|
component.Base
|
||||||
|
Dependencies struct {
|
||||||
|
Auth *auth.Auth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ component.ScopeProvider = (*UserLoggedIn)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
func (*UserLoggedIn) Scope() component.ScopeInfo {
|
||||||
|
return component.ScopeInfo{
|
||||||
|
Scope: component.ScopeUserLoggedIn,
|
||||||
|
Description: "session has an associated user",
|
||||||
|
TakesParam: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (iu *UserLoggedIn) HasScope(param string, r *http.Request) (bool, error) {
|
||||||
|
user, err := iu.Dependencies.Auth.UserOf(r)
|
||||||
|
return user != nil, err
|
||||||
|
}
|
||||||
|
|
@ -101,7 +101,8 @@ func (resolver *Resolver) HandleRoute(ctx context.Context, route string) (http.H
|
||||||
ctx := resolverContext{
|
ctx := resolverContext{
|
||||||
IndexContext: context,
|
IndexContext: context,
|
||||||
}
|
}
|
||||||
if !resolver.Dependencies.Auth.Has(auth.User, r) {
|
|
||||||
|
if resolver.Dependencies.Auth.CheckScope("", component.ScopeUserLoggedIn, r) != nil {
|
||||||
ctx.IndexContext.Prefixes = nil
|
ctx.IndexContext.Prefixes = nil
|
||||||
}
|
}
|
||||||
httpx.WriteHTML(tpl.Context(r, ctx), nil, t, "", w, r)
|
httpx.WriteHTML(tpl.Context(r, ctx), nil, t, "", w, r)
|
||||||
|
|
|
||||||
68
internal/dis/component/scope.go
Normal file
68
internal/dis/component/scope.go
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
package component
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Scope represents a single permit by a session to perform some action.
|
||||||
|
// Scopes consist of two parts: A general name and a specific object.
|
||||||
|
// Scopes are checked by ScopeCheckers.
|
||||||
|
type Scope string
|
||||||
|
|
||||||
|
type ScopeInfo struct {
|
||||||
|
Scope
|
||||||
|
|
||||||
|
// Description is a human readable description of the scope
|
||||||
|
Description string
|
||||||
|
|
||||||
|
// error returned to a user when the permission is denied.
|
||||||
|
// defaults to "missing scope {{ name }}"
|
||||||
|
DeniedMessage string
|
||||||
|
|
||||||
|
// TakesParam indicates if the scope accepts a parameter
|
||||||
|
TakesParam bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type CheckError struct {
|
||||||
|
Scope Scope
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ce CheckError) Unwrap() error { return ce.Err }
|
||||||
|
func (ce CheckError) Error() string {
|
||||||
|
return fmt.Sprintf("unable to check scope %q: %s", string(ce.Scope), ce.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
type AccessDeniedError string
|
||||||
|
|
||||||
|
func (aed AccessDeniedError) Error() string { return string(aed) }
|
||||||
|
|
||||||
|
// DeniedError returns an AccessDeniedError that indivates the access is denied.
|
||||||
|
func (scope ScopeInfo) DeniedError() error {
|
||||||
|
if scope.DeniedMessage == "" {
|
||||||
|
return AccessDeniedError(fmt.Sprintf("missing scope %q", string(scope.Scope)))
|
||||||
|
}
|
||||||
|
return AccessDeniedError(scope.DeniedMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckError returns a CheckError with the given underlying error.
|
||||||
|
func (scope ScopeInfo) CheckError(err error) error {
|
||||||
|
return CheckError{Scope: scope.Scope, Err: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
ScopeUserLoggedIn Scope = "login.user"
|
||||||
|
ScopeAdminLoggedIn Scope = "login.admin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ScopeProvider is a component that can check a specific scope
|
||||||
|
type ScopeProvider interface {
|
||||||
|
Component
|
||||||
|
|
||||||
|
// Scopes returns information about the scope
|
||||||
|
Scope() ScopeInfo
|
||||||
|
|
||||||
|
// Check checks if the given session has access to the given scope
|
||||||
|
HasScope(param string, r *http.Request) (bool, error)
|
||||||
|
}
|
||||||
|
|
@ -46,12 +46,12 @@ func (admin *Admin) Routes() component.Routes {
|
||||||
return component.Routes{
|
return component.Routes{
|
||||||
Prefix: "/admin/",
|
Prefix: "/admin/",
|
||||||
CSRF: true,
|
CSRF: true,
|
||||||
Decorator: admin.Dependencies.Auth.Require(auth.Admin),
|
Decorator: admin.Dependencies.Auth.Require(component.ScopeAdminLoggedIn, nil),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (admin *Admin) Menu(r *http.Request) []component.MenuItem {
|
func (admin *Admin) Menu(r *http.Request) []component.MenuItem {
|
||||||
if !admin.Dependencies.Auth.Has(auth.Admin, r) {
|
if admin.Dependencies.Auth.CheckScope("", component.ScopeAdminLoggedIn, r) != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return []component.MenuItem{
|
return []component.MenuItem{
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/next"
|
"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/panel"
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/policy"
|
"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/binder"
|
"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/docker"
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/exporter"
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/exporter"
|
||||||
|
|
@ -144,6 +145,10 @@ func (dis *Distillery) allComponents() []initFunc {
|
||||||
auto[*panel.UserPanel],
|
auto[*panel.UserPanel],
|
||||||
auto[*next.Next],
|
auto[*next.Next],
|
||||||
|
|
||||||
|
//scopes
|
||||||
|
auto[*scopes.UserLoggedIn],
|
||||||
|
auto[*scopes.AdminLoggedIn],
|
||||||
|
|
||||||
// instances
|
// instances
|
||||||
auto[*instances.Instances],
|
auto[*instances.Instances],
|
||||||
auto[*meta.Meta],
|
auto[*meta.Meta],
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue