Moved v3 code from NginxProxyManager/nginx-proxy-manager-3 to NginxProxyManager/nginx-proxy-manager
This commit is contained in:
13
backend/internal/api/middleware/access_control.go
Normal file
13
backend/internal/api/middleware/access_control.go
Normal file
@ -0,0 +1,13 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// AccessControl sets http headers for responses
|
||||
func AccessControl(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
94
backend/internal/api/middleware/auth.go
Normal file
94
backend/internal/api/middleware/auth.go
Normal file
@ -0,0 +1,94 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
c "npm/internal/api/context"
|
||||
h "npm/internal/api/http"
|
||||
"npm/internal/config"
|
||||
"npm/internal/entity/user"
|
||||
njwt "npm/internal/jwt"
|
||||
"npm/internal/logger"
|
||||
"npm/internal/util"
|
||||
|
||||
"github.com/go-chi/jwtauth"
|
||||
)
|
||||
|
||||
// DecodeAuth decodes an auth header
|
||||
func DecodeAuth() func(http.Handler) http.Handler {
|
||||
privateKey, privateKeyParseErr := njwt.GetPrivateKey()
|
||||
if privateKeyParseErr != nil && privateKey == nil {
|
||||
logger.Error("PrivateKeyParseError", privateKeyParseErr)
|
||||
}
|
||||
|
||||
publicKey, publicKeyParseErr := njwt.GetPublicKey()
|
||||
if publicKeyParseErr != nil && publicKey == nil {
|
||||
logger.Error("PublicKeyParseError", publicKeyParseErr)
|
||||
}
|
||||
|
||||
tokenAuth := jwtauth.New("RS256", privateKey, publicKey)
|
||||
return jwtauth.Verifier(tokenAuth)
|
||||
}
|
||||
|
||||
// Enforce is a authentication middleware to enforce access from the
|
||||
// jwtauth.Verifier middleware request context values. The Authenticator sends a 401 Unauthorised
|
||||
// response for any unverified tokens and passes the good ones through.
|
||||
func Enforce(permission string) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
if config.IsSetup {
|
||||
token, claims, err := jwtauth.FromContext(ctx)
|
||||
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusUnauthorized, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
userID := int(claims["uid"].(float64))
|
||||
_, enabled := user.IsEnabled(userID)
|
||||
if token == nil || !token.Valid || !enabled {
|
||||
h.ResultErrorJSON(w, r, http.StatusUnauthorized, "Unauthorised", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if permissions exist for this user
|
||||
if permission != "" {
|
||||
// Since the permission that we require is not on the token, we have to get it from the DB
|
||||
// So we don't go crazy with hits, we will use a memory cache
|
||||
cacheKey := fmt.Sprintf("userCapabilties.%v", userID)
|
||||
cacheItem, found := AuthCache.Get(cacheKey)
|
||||
|
||||
var userCapabilities []string
|
||||
if found {
|
||||
userCapabilities = cacheItem.([]string)
|
||||
} else {
|
||||
// Get from db and store it
|
||||
userCapabilities, err = user.GetCapabilities(userID)
|
||||
if err != nil {
|
||||
AuthCacheSet(cacheKey, userCapabilities)
|
||||
}
|
||||
}
|
||||
|
||||
// Now check that they have the permission in their admin capabilities
|
||||
// full-admin can do anything
|
||||
if !util.SliceContainsItem(userCapabilities, user.CapabilityFullAdmin) && !util.SliceContainsItem(userCapabilities, permission) {
|
||||
// Access denied
|
||||
logger.Debug("User has: %+v but needs %s", userCapabilities, permission)
|
||||
h.ResultErrorJSON(w, r, http.StatusForbidden, "Forbidden", nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Add claims to context
|
||||
ctx = context.WithValue(ctx, c.UserIDCtxKey, userID)
|
||||
}
|
||||
|
||||
// Token is authenticated, continue as normal
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
23
backend/internal/api/middleware/auth_cache.go
Normal file
23
backend/internal/api/middleware/auth_cache.go
Normal file
@ -0,0 +1,23 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"npm/internal/logger"
|
||||
|
||||
cache "github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
// AuthCache is a cache item that stores the Admin API data for each admin that has been requesting endpoints
|
||||
var AuthCache *cache.Cache
|
||||
|
||||
// AuthCacheInit will create a new Memory Cache
|
||||
func AuthCacheInit() {
|
||||
logger.Debug("Creating a new AuthCache")
|
||||
AuthCache = cache.New(1*time.Minute, 5*time.Minute)
|
||||
}
|
||||
|
||||
// AuthCacheSet will store the item in memory for the expiration time
|
||||
func AuthCacheSet(k string, x interface{}) {
|
||||
AuthCache.Set(k, x, cache.DefaultExpiration)
|
||||
}
|
26
backend/internal/api/middleware/body_context.go
Normal file
26
backend/internal/api/middleware/body_context.go
Normal file
@ -0,0 +1,26 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
c "npm/internal/api/context"
|
||||
)
|
||||
|
||||
// BodyContext simply adds the body data to a context item
|
||||
func BodyContext() func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Grab the Body Data
|
||||
var body []byte
|
||||
if r.Body != nil {
|
||||
body, _ = ioutil.ReadAll(r.Body)
|
||||
}
|
||||
// Add it to the context
|
||||
ctx := r.Context()
|
||||
ctx = context.WithValue(ctx, c.BodyCtxKey, body)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
88
backend/internal/api/middleware/cors.go
Normal file
88
backend/internal/api/middleware/cors.go
Normal file
@ -0,0 +1,88 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
)
|
||||
|
||||
var methodMap = []string{
|
||||
http.MethodGet,
|
||||
http.MethodHead,
|
||||
http.MethodPost,
|
||||
http.MethodPut,
|
||||
http.MethodPatch,
|
||||
http.MethodDelete,
|
||||
http.MethodConnect,
|
||||
http.MethodTrace,
|
||||
}
|
||||
|
||||
func getRouteMethods(routes chi.Router, path string) []string {
|
||||
var methods []string
|
||||
tctx := chi.NewRouteContext()
|
||||
for _, method := range methodMap {
|
||||
if routes.Match(tctx, method, path) {
|
||||
methods = append(methods, method)
|
||||
}
|
||||
}
|
||||
return methods
|
||||
}
|
||||
|
||||
var headersAllowedByCORS = []string{
|
||||
"Authorization",
|
||||
"Host",
|
||||
"Content-Type",
|
||||
"Connection",
|
||||
"User-Agent",
|
||||
"Cache-Control",
|
||||
"Accept-Encoding",
|
||||
"X-Jumbo-AppKey",
|
||||
"X-Jumbo-SKey",
|
||||
"X-Jumbo-SV",
|
||||
"X-Jumbo-Timestamp",
|
||||
"X-Jumbo-Version",
|
||||
"X-Jumbo-Customer-Id",
|
||||
}
|
||||
|
||||
// Cors handles cors headers
|
||||
func Cors(routes chi.Router) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
methods := getRouteMethods(routes, r.URL.Path)
|
||||
if len(methods) == 0 {
|
||||
// no route no cors
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
methods = append(methods, http.MethodOptions)
|
||||
w.Header().Set("Access-Control-Allow-Methods", strings.Join(methods, ","))
|
||||
w.Header().Set("Access-Control-Allow-Headers",
|
||||
strings.Join(headersAllowedByCORS, ","),
|
||||
)
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Options handles options requests
|
||||
func Options(routes chi.Router) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
methods := getRouteMethods(routes, r.URL.Path)
|
||||
if len(methods) == 0 {
|
||||
// no route shouldn't have options
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
if r.Method == http.MethodOptions {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprint(w, "{}")
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
28
backend/internal/api/middleware/enforce_setup.go
Normal file
28
backend/internal/api/middleware/enforce_setup.go
Normal file
@ -0,0 +1,28 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
h "npm/internal/api/http"
|
||||
"npm/internal/config"
|
||||
)
|
||||
|
||||
// EnforceSetup will error if the config setup doesn't match what is required
|
||||
func EnforceSetup(shouldBeSetup bool) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if config.IsSetup != shouldBeSetup {
|
||||
state := "during"
|
||||
if config.IsSetup {
|
||||
state = "after"
|
||||
}
|
||||
h.ResultErrorJSON(w, r, http.StatusForbidden, fmt.Sprintf("Not available %s setup phase", state), nil)
|
||||
return
|
||||
}
|
||||
|
||||
// All good
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
24
backend/internal/api/middleware/expansion.go
Normal file
24
backend/internal/api/middleware/expansion.go
Normal file
@ -0,0 +1,24 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
c "npm/internal/api/context"
|
||||
)
|
||||
|
||||
// Expansion will determine whether the request should have objects expanded
|
||||
// with ?expand=1 or ?expand=true
|
||||
func Expansion(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
expandStr := r.URL.Query().Get("expand")
|
||||
if expandStr != "" {
|
||||
ctx := r.Context()
|
||||
ctx = context.WithValue(ctx, c.ExpansionCtxKey, strings.Split(expandStr, ","))
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
} else {
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
})
|
||||
}
|
115
backend/internal/api/middleware/filters.go
Normal file
115
backend/internal/api/middleware/filters.go
Normal file
@ -0,0 +1,115 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
c "npm/internal/api/context"
|
||||
h "npm/internal/api/http"
|
||||
"npm/internal/model"
|
||||
"npm/internal/util"
|
||||
"strings"
|
||||
|
||||
"github.com/qri-io/jsonschema"
|
||||
)
|
||||
|
||||
// Filters will accept a pre-defined schemaData to validate against the GET query params
|
||||
// passed in to this endpoint. This will ensure that the filters are not injecting SQL.
|
||||
// After we have determined what the Filters are to be, they are saved on the Context
|
||||
// to be used later in other endpoints.
|
||||
func Filters(schemaData string) func(http.Handler) http.Handler {
|
||||
reservedFilterKeys := []string{
|
||||
"limit",
|
||||
"offset",
|
||||
"sort",
|
||||
"order",
|
||||
"expand",
|
||||
"t", // This is used as a timestamp paramater in some clients and can be ignored
|
||||
}
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var filters []model.Filter
|
||||
for key, val := range r.URL.Query() {
|
||||
key = strings.ToLower(key)
|
||||
|
||||
// Split out the modifier from the field name and set a default modifier
|
||||
var keyParts []string
|
||||
keyParts = strings.Split(key, ":")
|
||||
if len(keyParts) == 1 {
|
||||
// Default modifier
|
||||
keyParts = append(keyParts, "equals")
|
||||
}
|
||||
|
||||
// Only use this filter if it's not a reserved get param
|
||||
if !util.SliceContainsItem(reservedFilterKeys, keyParts[0]) {
|
||||
for _, valItem := range val {
|
||||
// Check that the val isn't empty
|
||||
if len(strings.TrimSpace(valItem)) > 0 {
|
||||
valSlice := []string{valItem}
|
||||
if keyParts[1] == "in" || keyParts[1] == "notin" {
|
||||
valSlice = strings.Split(valItem, ",")
|
||||
}
|
||||
|
||||
filters = append(filters, model.Filter{
|
||||
Field: keyParts[0],
|
||||
Modifier: keyParts[1],
|
||||
Value: valSlice,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only validate schema if there are filters to validate
|
||||
if len(filters) > 0 {
|
||||
ctx := r.Context()
|
||||
|
||||
// Marshal the Filters in to a JSON string so that the Schema Validation works against it
|
||||
filterData, marshalErr := json.MarshalIndent(filters, "", " ")
|
||||
if marshalErr != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusInternalServerError, fmt.Sprintf("Schema Fatal: %v", marshalErr), nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Create root schema
|
||||
rs := &jsonschema.Schema{}
|
||||
if err := json.Unmarshal([]byte(schemaData), rs); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusInternalServerError, fmt.Sprintf("Schema Fatal: %v", err), nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate it
|
||||
errors, jsonError := rs.ValidateBytes(ctx, filterData)
|
||||
if jsonError != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, jsonError.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, "Invalid Filters", errors)
|
||||
return
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, c.FiltersCtxKey, filters)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
|
||||
} else {
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// GetFiltersFromContext returns the Filters
|
||||
func GetFiltersFromContext(r *http.Request) []model.Filter {
|
||||
filters, ok := r.Context().Value(c.FiltersCtxKey).([]model.Filter)
|
||||
if !ok {
|
||||
// the assertion failed
|
||||
var emptyFilters []model.Filter
|
||||
return emptyFilters
|
||||
}
|
||||
return filters
|
||||
}
|
23
backend/internal/api/middleware/pretty_print.go
Normal file
23
backend/internal/api/middleware/pretty_print.go
Normal file
@ -0,0 +1,23 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
c "npm/internal/api/context"
|
||||
)
|
||||
|
||||
// PrettyPrint will determine whether the request should be pretty printed in output
|
||||
// with ?pretty=1 or ?pretty=true
|
||||
func PrettyPrint(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
prettyStr := r.URL.Query().Get("pretty")
|
||||
if prettyStr == "1" || prettyStr == "true" {
|
||||
ctx := r.Context()
|
||||
ctx = context.WithValue(ctx, c.PrettyPrintCtxKey, true)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
} else {
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
})
|
||||
}
|
55
backend/internal/api/middleware/schema.go
Normal file
55
backend/internal/api/middleware/schema.go
Normal file
@ -0,0 +1,55 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
c "npm/internal/api/context"
|
||||
h "npm/internal/api/http"
|
||||
|
||||
"github.com/qri-io/jsonschema"
|
||||
)
|
||||
|
||||
// CheckRequestSchema checks the payload against schema
|
||||
func CheckRequestSchema(ctx context.Context, schemaData string, payload []byte) ([]jsonschema.KeyError, error) {
|
||||
// Create root schema
|
||||
rs := &jsonschema.Schema{}
|
||||
if err := json.Unmarshal([]byte(schemaData), rs); err != nil {
|
||||
return nil, fmt.Errorf("Schema Fatal: %v", err)
|
||||
}
|
||||
|
||||
// Validate it
|
||||
schemaErrors, jsonError := rs.ValidateBytes(ctx, payload)
|
||||
if jsonError != nil {
|
||||
return nil, jsonError
|
||||
}
|
||||
|
||||
return schemaErrors, nil
|
||||
}
|
||||
|
||||
// EnforceRequestSchema accepts a schema and validates the request body against it
|
||||
func EnforceRequestSchema(schemaData string) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Get content from context
|
||||
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
||||
|
||||
schemaErrors, err := CheckRequestSchema(r.Context(), schemaData, bodyBytes)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if len(schemaErrors) > 0 {
|
||||
h.ResultSchemaErrorJSON(w, r, schemaErrors)
|
||||
return
|
||||
}
|
||||
|
||||
// All good
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user