satellite/admin/back-office: Add auth middleware
Create an API generator middleware for being able to hook the new satellite admin authorization in the endpoints. The commit fixes a bug found in the API generator that caused that fields of types of the same package of the generated code where wrongly added. Concretely: - The package matching was missing in the function middlewareFields, hence it was generating code that referenced types with the package name. - middlewareFields function was not adding the pointer symbol (*) when the type was from the same package where the generated code is written. There is also an accidental enhancement in the API generator because I thought that the bug commented above corresponded to it, rather than removing it, I though that was worthwhile to keep it because it was already implemented. This enhancement allows to use fields in the middleware with packages whose last path part contains `-` or `.`, using a package rename in the import statement. Change-Id: Ie98b303226a8e8845e494f25054867f95a283aa0
This commit is contained in:
parent
0467903b52
commit
fb31761bad
@ -6,7 +6,6 @@ package apigen
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
@ -348,7 +347,7 @@ func middlewareImports(m any) []string {
|
|||||||
|
|
||||||
// middlewareFields returns the list of fields of a middleware implementation. It panics if m isn't
|
// middlewareFields returns the list of fields of a middleware implementation. It panics if m isn't
|
||||||
// a struct type, it has embedded fields, or it has unexported fields.
|
// a struct type, it has embedded fields, or it has unexported fields.
|
||||||
func middlewareFields(m any) []middlewareField {
|
func middlewareFields(api *API, m any) []middlewareField {
|
||||||
fields := []middlewareField{}
|
fields := []middlewareField{}
|
||||||
middlewareWalkFields(m, func(f reflect.StructField) {
|
middlewareWalkFields(m, func(f reflect.StructField) {
|
||||||
if f.Name == "_" {
|
if f.Name == "_" {
|
||||||
@ -362,9 +361,10 @@ func middlewareFields(m any) []middlewareField {
|
|||||||
t = f.Type.Elem()
|
t = f.Type.Elem()
|
||||||
}
|
}
|
||||||
|
|
||||||
typeref := t.Name()
|
typeref := psymbol + t.Name()
|
||||||
if p := t.PkgPath(); p != "" {
|
if p := t.PkgPath(); p != "" && p != api.PackagePath {
|
||||||
typeref = fmt.Sprintf("%s%s.%s", psymbol, filepath.Base(p), typeref)
|
pn, _ := importPath(p).PkgName()
|
||||||
|
typeref = fmt.Sprintf("%s%s.%s", psymbol, pn, t.Name())
|
||||||
}
|
}
|
||||||
fields = append(fields, middlewareField{Name: f.Name, Type: typeref})
|
fields = append(fields, middlewareField{Name: f.Name, Type: typeref})
|
||||||
})
|
})
|
||||||
|
@ -7,8 +7,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"go/format"
|
"go/format"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sort"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -50,12 +51,12 @@ func (a *API) generateGo() ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
imports := struct {
|
imports := struct {
|
||||||
All map[string]bool
|
All map[importPath]bool
|
||||||
Standard []string
|
Standard []importPath
|
||||||
External []string
|
External []importPath
|
||||||
Internal []string
|
Internal []importPath
|
||||||
}{
|
}{
|
||||||
All: make(map[string]bool),
|
All: make(map[importPath]bool),
|
||||||
}
|
}
|
||||||
|
|
||||||
i := func(paths ...string) {
|
i := func(paths ...string) {
|
||||||
@ -64,12 +65,13 @@ func (a *API) generateGo() ([]byte, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := imports.All[path]; ok {
|
ipath := importPath(path)
|
||||||
|
if _, ok := imports.All[ipath]; ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
imports.All[path] = true
|
imports.All[ipath] = true
|
||||||
|
|
||||||
var slice *[]string
|
var slice *[]importPath
|
||||||
switch {
|
switch {
|
||||||
case !strings.Contains(path, "."):
|
case !strings.Contains(path, "."):
|
||||||
slice = &imports.Standard
|
slice = &imports.Standard
|
||||||
@ -78,7 +80,7 @@ func (a *API) generateGo() ([]byte, error) {
|
|||||||
default:
|
default:
|
||||||
slice = &imports.External
|
slice = &imports.External
|
||||||
}
|
}
|
||||||
*slice = append(*slice, path)
|
*slice = append(*slice, ipath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,12 +172,12 @@ func (a *API) generateGo() ([]byte, error) {
|
|||||||
|
|
||||||
autodefinedFields := map[string]string{"log": "*zap.Logger", "mon": "*monkit.Scope", "service": cname + "Service"}
|
autodefinedFields := map[string]string{"log": "*zap.Logger", "mon": "*monkit.Scope", "service": cname + "Service"}
|
||||||
for _, m := range group.Middleware {
|
for _, m := range group.Middleware {
|
||||||
for _, f := range middlewareFields(m) {
|
for _, f := range middlewareFields(a, m) {
|
||||||
if t, ok := autodefinedFields[f.Name]; ok {
|
if t, ok := autodefinedFields[f.Name]; ok {
|
||||||
if t != f.Type {
|
if t != f.Type {
|
||||||
panic(
|
panic(
|
||||||
fmt.Sprintf(
|
fmt.Sprintf(
|
||||||
"middleware %q has a field with name %q and type %q which clashes with another defined field with the same but with type %q",
|
"middleware %q has a field with name %q and type %q which clashes with another defined field with the same name but with type %q",
|
||||||
reflect.TypeOf(m).Name(),
|
reflect.TypeOf(m).Name(),
|
||||||
f.Name,
|
f.Name,
|
||||||
f.Type,
|
f.Type,
|
||||||
@ -203,7 +205,7 @@ func (a *API) generateGo() ([]byte, error) {
|
|||||||
middlewareArgs := make([]string, 0, len(group.Middleware))
|
middlewareArgs := make([]string, 0, len(group.Middleware))
|
||||||
middlewareFieldsList := make([]string, 0, len(group.Middleware))
|
middlewareFieldsList := make([]string, 0, len(group.Middleware))
|
||||||
for _, m := range group.Middleware {
|
for _, m := range group.Middleware {
|
||||||
for _, f := range middlewareFields(m) {
|
for _, f := range middlewareFields(a, m) {
|
||||||
if _, ok := autodedefined[f.Name]; !ok {
|
if _, ok := autodedefined[f.Name]; !ok {
|
||||||
middlewareArgs = append(middlewareArgs, fmt.Sprintf("%s %s", f.Name, f.Type))
|
middlewareArgs = append(middlewareArgs, fmt.Sprintf("%s %s", f.Name, f.Type))
|
||||||
middlewareFieldsList = append(middlewareFieldsList, fmt.Sprintf("%[1]s: %[1]s", f.Name))
|
middlewareFieldsList = append(middlewareFieldsList, fmt.Sprintf("%[1]s: %[1]s", f.Name))
|
||||||
@ -339,12 +341,17 @@ func (a *API) generateGo() ([]byte, error) {
|
|||||||
pf("")
|
pf("")
|
||||||
|
|
||||||
pf("import (")
|
pf("import (")
|
||||||
slices := [][]string{imports.Standard, imports.External, imports.Internal}
|
all := [][]importPath{imports.Standard, imports.External, imports.Internal}
|
||||||
for sn, slice := range slices {
|
for sn, slice := range all {
|
||||||
sort.Strings(slice)
|
slices.Sort(slice)
|
||||||
for pn, path := range slice {
|
for pn, path := range slice {
|
||||||
pf(`"%s"`, path)
|
if r, ok := path.PkgName(); ok {
|
||||||
if pn == len(slice)-1 && sn < len(slices)-1 {
|
pf(`%s "%s"`, r, path)
|
||||||
|
} else {
|
||||||
|
pf(`"%s"`, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pn == len(slice)-1 && sn < len(all)-1 {
|
||||||
pf("")
|
pf("")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -469,3 +476,20 @@ func handleBody(pf func(format string, a ...interface{}), body interface{}) {
|
|||||||
pf("}")
|
pf("}")
|
||||||
pf("")
|
pf("")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type importPath string
|
||||||
|
|
||||||
|
// PkgName returns the name of the package based of the last part of the import
|
||||||
|
// path and false if the name isn't a rename, otherwise it returns true.
|
||||||
|
//
|
||||||
|
// The package name is renamed when the last part of the path contains hyphen
|
||||||
|
// (-) or dot (.) and the rename is this part with the hyphens and dots
|
||||||
|
// stripped.
|
||||||
|
func (i importPath) PkgName() (rename string, ok bool) {
|
||||||
|
b := filepath.Base(string(i))
|
||||||
|
if strings.Contains(b, "-") || strings.Contains(b, ".") {
|
||||||
|
return strings.ReplaceAll(strings.ReplaceAll(b, "-", ""), ".", ""), true
|
||||||
|
}
|
||||||
|
|
||||||
|
return b, false
|
||||||
|
}
|
||||||
|
@ -8,9 +8,11 @@ package main
|
|||||||
//go:generate go run $GOFILE
|
//go:generate go run $GOFILE
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"storj.io/storj/private/apigen"
|
"storj.io/storj/private/apigen"
|
||||||
backoffice "storj.io/storj/satellite/admin/back-office"
|
backoffice "storj.io/storj/satellite/admin/back-office"
|
||||||
@ -35,6 +37,7 @@ func main() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
group = api.Group("UserManagement", "users")
|
group = api.Group("UserManagement", "users")
|
||||||
|
group.Middleware = append(group.Middleware, authMiddleware{})
|
||||||
|
|
||||||
group.Get("/{email}", &apigen.Endpoint{
|
group.Get("/{email}", &apigen.Endpoint{
|
||||||
Name: "Get user",
|
Name: "Get user",
|
||||||
@ -45,6 +48,9 @@ func main() {
|
|||||||
apigen.NewParam("email", ""),
|
apigen.NewParam("email", ""),
|
||||||
},
|
},
|
||||||
Response: backoffice.User{},
|
Response: backoffice.User{},
|
||||||
|
Settings: map[any]any{
|
||||||
|
authPermsKey: []backoffice.Permission{backoffice.PermAccountView},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
modroot := findModuleRootDir()
|
modroot := findModuleRootDir()
|
||||||
@ -53,6 +59,37 @@ func main() {
|
|||||||
api.MustWriteDocs(filepath.Join(modroot, "satellite", "admin", "back-office", "api-docs.gen.md"))
|
api.MustWriteDocs(filepath.Join(modroot, "satellite", "admin", "back-office", "api-docs.gen.md"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type authMiddleware struct {
|
||||||
|
//lint:ignore U1000 this field is used by the API generator to expose in the handler.
|
||||||
|
auth *backoffice.Authorizer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a authMiddleware) Generate(api *apigen.API, group *apigen.EndpointGroup, ep *apigen.FullEndpoint) string {
|
||||||
|
perms := apigen.LoadSetting(authPermsKey, ep, []backoffice.Permission{})
|
||||||
|
if len(perms) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
verbs := make([]string, 0, len(perms))
|
||||||
|
values := make([]any, 0, len(perms))
|
||||||
|
for _, p := range perms {
|
||||||
|
verbs = append(verbs, "%d")
|
||||||
|
values = append(values, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
format := fmt.Sprintf(`if h.auth.IsRejected(w, r, %s) {
|
||||||
|
return
|
||||||
|
}`, strings.Join(verbs, ", "))
|
||||||
|
|
||||||
|
return fmt.Sprintf(format, values...)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ apigen.Middleware = authMiddleware{}
|
||||||
|
|
||||||
|
type tagAuthPerms struct{}
|
||||||
|
|
||||||
|
var authPermsKey = tagAuthPerms{}
|
||||||
|
|
||||||
func findModuleRootDir() string {
|
func findModuleRootDir() string {
|
||||||
dir, err := os.Getwd()
|
dir, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -39,6 +39,7 @@ type UserManagementHandler struct {
|
|||||||
log *zap.Logger
|
log *zap.Logger
|
||||||
mon *monkit.Scope
|
mon *monkit.Scope
|
||||||
service UserManagementService
|
service UserManagementService
|
||||||
|
auth *Authorizer
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPlacementManagement(log *zap.Logger, mon *monkit.Scope, service PlacementManagementService, router *mux.Router) *PlacementManagementHandler {
|
func NewPlacementManagement(log *zap.Logger, mon *monkit.Scope, service PlacementManagementService, router *mux.Router) *PlacementManagementHandler {
|
||||||
@ -54,11 +55,12 @@ func NewPlacementManagement(log *zap.Logger, mon *monkit.Scope, service Placemen
|
|||||||
return handler
|
return handler
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUserManagement(log *zap.Logger, mon *monkit.Scope, service UserManagementService, router *mux.Router) *UserManagementHandler {
|
func NewUserManagement(log *zap.Logger, mon *monkit.Scope, service UserManagementService, router *mux.Router, auth *Authorizer) *UserManagementHandler {
|
||||||
handler := &UserManagementHandler{
|
handler := &UserManagementHandler{
|
||||||
log: log,
|
log: log,
|
||||||
mon: mon,
|
mon: mon,
|
||||||
service: service,
|
service: service,
|
||||||
|
auth: auth,
|
||||||
}
|
}
|
||||||
|
|
||||||
usersRouter := router.PathPrefix("/back-office/api/v1/users").Subrouter()
|
usersRouter := router.PathPrefix("/back-office/api/v1/users").Subrouter()
|
||||||
@ -99,6 +101,10 @@ func (h *UserManagementHandler) handleGetUserByEmail(w http.ResponseWriter, r *h
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if h.auth.IsRejected(w, r, 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
retVal, httpErr := h.service.GetUserByEmail(ctx, email)
|
retVal, httpErr := h.service.GetUserByEmail(ctx, email)
|
||||||
if httpErr.Err != nil {
|
if httpErr.Err != nil {
|
||||||
api.ServeError(h.log, w, httpErr.Status, httpErr.Err)
|
api.ServeError(h.log, w, httpErr.Status, httpErr.Err)
|
||||||
|
@ -72,10 +72,18 @@ func NewServer(
|
|||||||
root = mux.NewRouter()
|
root = mux.NewRouter()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auth := NewAuthorizer(
|
||||||
|
log,
|
||||||
|
config.UserGroupsRoleAdmin,
|
||||||
|
config.UserGroupsRoleViewer,
|
||||||
|
config.UserGroupsRoleCustomerSupport,
|
||||||
|
config.UserGroupsRoleFinanceManager,
|
||||||
|
)
|
||||||
|
|
||||||
// API endpoints.
|
// API endpoints.
|
||||||
// API generator already add the PathPrefix.
|
// API generator already add the PathPrefix.
|
||||||
NewPlacementManagement(log, mon, service, root)
|
NewPlacementManagement(log, mon, service, root)
|
||||||
NewUserManagement(log, mon, service, root)
|
NewUserManagement(log, mon, service, root, auth)
|
||||||
|
|
||||||
root = root.PathPrefix(PathPrefix).Subrouter()
|
root = root.PathPrefix(PathPrefix).Subrouter()
|
||||||
// Static assets for the web interface.
|
// Static assets for the web interface.
|
||||||
|
Loading…
Reference in New Issue
Block a user