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:
Ivan Fraixedes 2023-11-24 13:23:01 +01:00 committed by Storj Robot
parent 0467903b52
commit fb31761bad
5 changed files with 100 additions and 25 deletions

View File

@ -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})
})

View File

@ -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
}

View File

@ -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 {

View File

@ -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)

View File

@ -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.