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 (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"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
|
||||
// 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{}
|
||||
middlewareWalkFields(m, func(f reflect.StructField) {
|
||||
if f.Name == "_" {
|
||||
@ -362,9 +361,10 @@ func middlewareFields(m any) []middlewareField {
|
||||
t = f.Type.Elem()
|
||||
}
|
||||
|
||||
typeref := t.Name()
|
||||
if p := t.PkgPath(); p != "" {
|
||||
typeref = fmt.Sprintf("%s%s.%s", psymbol, filepath.Base(p), typeref)
|
||||
typeref := psymbol + t.Name()
|
||||
if p := t.PkgPath(); p != "" && p != api.PackagePath {
|
||||
pn, _ := importPath(p).PkgName()
|
||||
typeref = fmt.Sprintf("%s%s.%s", psymbol, pn, t.Name())
|
||||
}
|
||||
fields = append(fields, middlewareField{Name: f.Name, Type: typeref})
|
||||
})
|
||||
|
@ -7,8 +7,9 @@ import (
|
||||
"fmt"
|
||||
"go/format"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sort"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -50,12 +51,12 @@ func (a *API) generateGo() ([]byte, error) {
|
||||
}
|
||||
|
||||
imports := struct {
|
||||
All map[string]bool
|
||||
Standard []string
|
||||
External []string
|
||||
Internal []string
|
||||
All map[importPath]bool
|
||||
Standard []importPath
|
||||
External []importPath
|
||||
Internal []importPath
|
||||
}{
|
||||
All: make(map[string]bool),
|
||||
All: make(map[importPath]bool),
|
||||
}
|
||||
|
||||
i := func(paths ...string) {
|
||||
@ -64,12 +65,13 @@ func (a *API) generateGo() ([]byte, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := imports.All[path]; ok {
|
||||
ipath := importPath(path)
|
||||
if _, ok := imports.All[ipath]; ok {
|
||||
continue
|
||||
}
|
||||
imports.All[path] = true
|
||||
imports.All[ipath] = true
|
||||
|
||||
var slice *[]string
|
||||
var slice *[]importPath
|
||||
switch {
|
||||
case !strings.Contains(path, "."):
|
||||
slice = &imports.Standard
|
||||
@ -78,7 +80,7 @@ func (a *API) generateGo() ([]byte, error) {
|
||||
default:
|
||||
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"}
|
||||
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 != f.Type {
|
||||
panic(
|
||||
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(),
|
||||
f.Name,
|
||||
f.Type,
|
||||
@ -203,7 +205,7 @@ func (a *API) generateGo() ([]byte, error) {
|
||||
middlewareArgs := make([]string, 0, len(group.Middleware))
|
||||
middlewareFieldsList := make([]string, 0, len(group.Middleware))
|
||||
for _, m := range group.Middleware {
|
||||
for _, f := range middlewareFields(m) {
|
||||
for _, f := range middlewareFields(a, m) {
|
||||
if _, ok := autodedefined[f.Name]; !ok {
|
||||
middlewareArgs = append(middlewareArgs, fmt.Sprintf("%s %s", f.Name, f.Type))
|
||||
middlewareFieldsList = append(middlewareFieldsList, fmt.Sprintf("%[1]s: %[1]s", f.Name))
|
||||
@ -339,12 +341,17 @@ func (a *API) generateGo() ([]byte, error) {
|
||||
pf("")
|
||||
|
||||
pf("import (")
|
||||
slices := [][]string{imports.Standard, imports.External, imports.Internal}
|
||||
for sn, slice := range slices {
|
||||
sort.Strings(slice)
|
||||
all := [][]importPath{imports.Standard, imports.External, imports.Internal}
|
||||
for sn, slice := range all {
|
||||
slices.Sort(slice)
|
||||
for pn, path := range slice {
|
||||
pf(`"%s"`, path)
|
||||
if pn == len(slice)-1 && sn < len(slices)-1 {
|
||||
if r, ok := path.PkgName(); ok {
|
||||
pf(`%s "%s"`, r, path)
|
||||
} else {
|
||||
pf(`"%s"`, path)
|
||||
}
|
||||
|
||||
if pn == len(slice)-1 && sn < len(all)-1 {
|
||||
pf("")
|
||||
}
|
||||
}
|
||||
@ -469,3 +476,20 @@ func handleBody(pf func(format string, a ...interface{}), body interface{}) {
|
||||
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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"storj.io/storj/private/apigen"
|
||||
backoffice "storj.io/storj/satellite/admin/back-office"
|
||||
@ -35,6 +37,7 @@ func main() {
|
||||
})
|
||||
|
||||
group = api.Group("UserManagement", "users")
|
||||
group.Middleware = append(group.Middleware, authMiddleware{})
|
||||
|
||||
group.Get("/{email}", &apigen.Endpoint{
|
||||
Name: "Get user",
|
||||
@ -45,6 +48,9 @@ func main() {
|
||||
apigen.NewParam("email", ""),
|
||||
},
|
||||
Response: backoffice.User{},
|
||||
Settings: map[any]any{
|
||||
authPermsKey: []backoffice.Permission{backoffice.PermAccountView},
|
||||
},
|
||||
})
|
||||
|
||||
modroot := findModuleRootDir()
|
||||
@ -53,6 +59,37 @@ func main() {
|
||||
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 {
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
|
@ -39,6 +39,7 @@ type UserManagementHandler struct {
|
||||
log *zap.Logger
|
||||
mon *monkit.Scope
|
||||
service UserManagementService
|
||||
auth *Authorizer
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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{
|
||||
log: log,
|
||||
mon: mon,
|
||||
service: service,
|
||||
auth: auth,
|
||||
}
|
||||
|
||||
usersRouter := router.PathPrefix("/back-office/api/v1/users").Subrouter()
|
||||
@ -99,6 +101,10 @@ func (h *UserManagementHandler) handleGetUserByEmail(w http.ResponseWriter, r *h
|
||||
return
|
||||
}
|
||||
|
||||
if h.auth.IsRejected(w, r, 1) {
|
||||
return
|
||||
}
|
||||
|
||||
retVal, httpErr := h.service.GetUserByEmail(ctx, email)
|
||||
if httpErr.Err != nil {
|
||||
api.ServeError(h.log, w, httpErr.Status, httpErr.Err)
|
||||
|
@ -72,10 +72,18 @@ func NewServer(
|
||||
root = mux.NewRouter()
|
||||
}
|
||||
|
||||
auth := NewAuthorizer(
|
||||
log,
|
||||
config.UserGroupsRoleAdmin,
|
||||
config.UserGroupsRoleViewer,
|
||||
config.UserGroupsRoleCustomerSupport,
|
||||
config.UserGroupsRoleFinanceManager,
|
||||
)
|
||||
|
||||
// API endpoints.
|
||||
// API generator already add the PathPrefix.
|
||||
NewPlacementManagement(log, mon, service, root)
|
||||
NewUserManagement(log, mon, service, root)
|
||||
NewUserManagement(log, mon, service, root, auth)
|
||||
|
||||
root = root.PathPrefix(PathPrefix).Subrouter()
|
||||
// Static assets for the web interface.
|
||||
|
Loading…
Reference in New Issue
Block a user