mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-19 08:46:38 +00:00
move to reverse proxy and update api
This commit is contained in:
125
management/internals/modules/reverseproxy/domains/api.go
Normal file
125
management/internals/modules/reverseproxy/domains/api.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package domains
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
nbcontext "github.com/netbirdio/netbird/management/server/context"
|
||||
"github.com/netbirdio/netbird/shared/management/http/api"
|
||||
"github.com/netbirdio/netbird/shared/management/http/util"
|
||||
"github.com/netbirdio/netbird/shared/management/status"
|
||||
)
|
||||
|
||||
type handler struct {
|
||||
manager manager
|
||||
}
|
||||
|
||||
func RegisterEndpoints(router *mux.Router) {
|
||||
h := &handler{}
|
||||
|
||||
router.HandleFunc("/domains", h.getAllDomains).Methods("GET", "OPTIONS")
|
||||
router.HandleFunc("/domains", h.createCustomDomain).Methods("POST", "OPTIONS")
|
||||
router.HandleFunc("/domains/{domainId}", h.deleteCustomDomain).Methods("DELETE", "OPTIONS")
|
||||
router.HandleFunc("/domains/{domainId}/validate", h.triggerCustomDomainValidation).Methods("GET", "OPTIONS")
|
||||
}
|
||||
|
||||
func domainTypeToApi(t domainType) api.ReverseProxyDomainType {
|
||||
switch t {
|
||||
case domainTypeCustom:
|
||||
return api.ReverseProxyDomainTypeCustom
|
||||
case domainTypeFree:
|
||||
return api.ReverseProxyDomainTypeFree
|
||||
}
|
||||
// By default return as a "free" domain as that is more restrictive.
|
||||
// TODO: is this correct?
|
||||
return api.ReverseProxyDomainTypeFree
|
||||
}
|
||||
|
||||
func domainToApi(d domain) api.ReverseProxyDomain {
|
||||
return api.ReverseProxyDomain{
|
||||
Domain: d.Domain,
|
||||
Id: d.ID,
|
||||
Type: domainTypeToApi(d.Type),
|
||||
Validated: d.Validated,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) getAllDomains(w http.ResponseWriter, r *http.Request) {
|
||||
userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
|
||||
if err != nil {
|
||||
util.WriteError(r.Context(), err, w)
|
||||
return
|
||||
}
|
||||
|
||||
domains, err := h.manager.GetDomains(r.Context(), userAuth.AccountId)
|
||||
|
||||
var ret []api.ReverseProxyDomain
|
||||
for _, d := range domains {
|
||||
ret = append(ret, domainToApi(d))
|
||||
}
|
||||
|
||||
util.WriteJSONObject(r.Context(), w, ret)
|
||||
}
|
||||
|
||||
func (h *handler) createCustomDomain(w http.ResponseWriter, r *http.Request) {
|
||||
userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
|
||||
if err != nil {
|
||||
util.WriteError(r.Context(), err, w)
|
||||
return
|
||||
}
|
||||
|
||||
var req api.PostApiReverseProxyDomainsJSONRequestBody
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
util.WriteErrorResponse("couldn't parse JSON request", http.StatusBadRequest, w)
|
||||
return
|
||||
}
|
||||
|
||||
domain, err := h.manager.CreateDomain(r.Context(), userAuth.AccountId, req.Domain)
|
||||
if err != nil {
|
||||
util.WriteError(r.Context(), err, w)
|
||||
return
|
||||
}
|
||||
|
||||
util.WriteJSONObject(r.Context(), w, domainToApi(domain))
|
||||
}
|
||||
|
||||
func (h *handler) deleteCustomDomain(w http.ResponseWriter, r *http.Request) {
|
||||
userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
|
||||
if err != nil {
|
||||
util.WriteError(r.Context(), err, w)
|
||||
return
|
||||
}
|
||||
|
||||
domainID := mux.Vars(r)["domainId"]
|
||||
if domainID == "" {
|
||||
util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "domain ID is required"), w)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.manager.DeleteDomain(r.Context(), userAuth.AccountId, domainID); err != nil {
|
||||
util.WriteError(r.Context(), err, w)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *handler) triggerCustomDomainValidation(w http.ResponseWriter, r *http.Request) {
|
||||
userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
|
||||
if err != nil {
|
||||
util.WriteError(r.Context(), err, w)
|
||||
return
|
||||
}
|
||||
|
||||
domainID := mux.Vars(r)["domainId"]
|
||||
if domainID == "" {
|
||||
util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "domain ID is required"), w)
|
||||
return
|
||||
}
|
||||
|
||||
go h.manager.ValidateDomain(userAuth.AccountId, domainID)
|
||||
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
}
|
||||
109
management/internals/modules/reverseproxy/domains/manager.go
Normal file
109
management/internals/modules/reverseproxy/domains/manager.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package domains
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type domainType string
|
||||
|
||||
const (
|
||||
domainTypeFree domainType = "free"
|
||||
domainTypeCustom domainType = "custom"
|
||||
)
|
||||
|
||||
type domain struct {
|
||||
ID string
|
||||
Domain string
|
||||
Type domainType
|
||||
Validated bool
|
||||
}
|
||||
|
||||
type store interface {
|
||||
GetAccountDomainNonce(ctx context.Context, accountID string) (nonce string, err error)
|
||||
GetCustomDomain(ctx context.Context, accountID string, domainID string) (domain, error)
|
||||
ListFreeDomains(ctx context.Context, accountID string) ([]string, error)
|
||||
ListCustomDomains(ctx context.Context, accountID string) ([]domain, error)
|
||||
CreateCustomDomain(ctx context.Context, accountID string, domainName string, validated bool) (domain, error)
|
||||
UpdateCustomDomain(ctx context.Context, accountID string, d domain) (domain, error)
|
||||
DeleteCustomDomain(ctx context.Context, accountID string, domainID string) error
|
||||
}
|
||||
|
||||
type manager struct {
|
||||
store store
|
||||
validator Validator
|
||||
}
|
||||
|
||||
func (m manager) GetDomains(ctx context.Context, accountID string) ([]domain, error) {
|
||||
nonce, err := m.store.GetAccountDomainNonce(ctx, accountID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get account domain nonce: %w", err)
|
||||
}
|
||||
free, err := m.store.ListFreeDomains(ctx, accountID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list free domains: %w", err)
|
||||
}
|
||||
domains, err := m.store.ListCustomDomains(ctx, accountID)
|
||||
if err != nil {
|
||||
// TODO: check for "no records" type error. Because that is a success condition.
|
||||
return nil, fmt.Errorf("list custom domains: %w", err)
|
||||
}
|
||||
|
||||
// Prepend each free domain with the account nonce and then add it to the domain
|
||||
// array to be returned.
|
||||
// This account nonce is added to free domains to prevent users being able to
|
||||
// query free domain usage across accounts and simplifies tracking free domain
|
||||
// usage across accounts.
|
||||
for _, name := range free {
|
||||
domains = append(domains, domain{
|
||||
Domain: nonce + "." + name,
|
||||
Type: domainTypeFree,
|
||||
Validated: true,
|
||||
})
|
||||
}
|
||||
return domains, nil
|
||||
}
|
||||
|
||||
func (m manager) CreateDomain(ctx context.Context, accountID, domainName string) (domain, error) {
|
||||
// Attempt an initial validation; however, a failure is still acceptable for creation
|
||||
// because the user may not yet have configured their DNS records, or the DNS update
|
||||
// has not yet reached the servers that are queried by the validation resolver.
|
||||
var validated bool
|
||||
// TODO: retrieve in use reverse proxy addresses from somewhere!
|
||||
var reverseProxyAddresses []string
|
||||
if m.validator.IsValid(ctx, domainName, reverseProxyAddresses) {
|
||||
validated = true
|
||||
}
|
||||
|
||||
d, err := m.store.CreateCustomDomain(ctx, accountID, domainName, validated)
|
||||
if err != nil {
|
||||
return d, fmt.Errorf("create domain in store: %w", err)
|
||||
}
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (m manager) DeleteDomain(ctx context.Context, accountID, domainID string) error {
|
||||
if err := m.store.DeleteCustomDomain(ctx, accountID, domainID); err != nil {
|
||||
// TODO: check for "no records" type error. Because that is a success condition.
|
||||
return fmt.Errorf("delete domain from store: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m manager) ValidateDomain(accountID, domainID string) {
|
||||
d, err := m.store.GetCustomDomain(context.Background(), accountID, domainID)
|
||||
if err != nil {
|
||||
// TODO: something? Log?
|
||||
return
|
||||
}
|
||||
// TODO: retrieve in use reverse proxy addresses from somewhere!
|
||||
var reverseProxyAddresses []string
|
||||
if m.validator.IsValid(context.Background(), d.Domain, reverseProxyAddresses) {
|
||||
d.Validated = true
|
||||
if _, err := m.store.UpdateCustomDomain(context.Background(), accountID, d); err != nil {
|
||||
// TODO: something? Log?
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package domains
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type resolver interface {
|
||||
LookupCNAME(context.Context, string) (string, error)
|
||||
}
|
||||
|
||||
type Validator struct {
|
||||
resolver resolver
|
||||
}
|
||||
|
||||
// NewValidator initializes a validator with a specific DNS resolver.
|
||||
// If a Validator is used without specifying a resolver, then it will
|
||||
// use the net.DefaultResolver.
|
||||
func NewValidator(resolver resolver) *Validator {
|
||||
return &Validator{
|
||||
resolver: resolver,
|
||||
}
|
||||
}
|
||||
|
||||
// IsValid looks up the CNAME record for the passed domain and compares it
|
||||
// against the acceptable domains.
|
||||
// If the returned CNAME matches any accepted domain, it will return true,
|
||||
// otherwise, including in the event of a DNS error, it will return false.
|
||||
// The comparison is very simple, so wildcards will not match if included
|
||||
// in the acceptable domain list.
|
||||
func (v *Validator) IsValid(ctx context.Context, domain string, accept []string) bool {
|
||||
if v.resolver == nil {
|
||||
v.resolver = net.DefaultResolver
|
||||
}
|
||||
|
||||
cname, err := v.resolver.LookupCNAME(ctx, domain)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Remove a trailing "." from the CNAME (most people do not include the trailing "." in FQDN, so it is easier to strip this when comparing).
|
||||
nakedCNAME := strings.TrimSuffix(cname, ".")
|
||||
for _, domain := range accept {
|
||||
// Currently, the match is a very simple string comparison.
|
||||
if nakedCNAME == strings.TrimSuffix(domain, ".") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package domains_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/domains"
|
||||
)
|
||||
|
||||
type resolver struct {
|
||||
CNAME string
|
||||
}
|
||||
|
||||
func (r resolver) LookupCNAME(_ context.Context, _ string) (string, error) {
|
||||
return r.CNAME, nil
|
||||
}
|
||||
|
||||
func TestIsValid(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
resolver interface {
|
||||
LookupCNAME(context.Context, string) (string, error)
|
||||
}
|
||||
domain string
|
||||
accept []string
|
||||
expect bool
|
||||
}{
|
||||
"match": {
|
||||
resolver: resolver{"bar.example.com."}, // Including trailing "." in response.
|
||||
domain: "foo.example.com",
|
||||
accept: []string{"bar.example.com"},
|
||||
expect: true,
|
||||
},
|
||||
"no match": {
|
||||
resolver: resolver{"invalid"},
|
||||
domain: "foo.example.com",
|
||||
accept: []string{"bar.example.com"},
|
||||
expect: false,
|
||||
},
|
||||
"accept trailing dot": {
|
||||
resolver: resolver{"bar.example.com."},
|
||||
domain: "foo.example.com",
|
||||
accept: []string{"bar.example.com."}, // Including trailing "." in accept.
|
||||
expect: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
validator := domains.NewValidator(test.resolver)
|
||||
actual := validator.IsValid(t.Context(), test.domain, test.accept)
|
||||
if test.expect != actual {
|
||||
t.Errorf("Incorrect return value:\nexpect: %v\nactual: %v", test.expect, actual)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
13
management/internals/modules/reverseproxy/interface.go
Normal file
13
management/internals/modules/reverseproxy/interface.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package reverseproxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type Manager interface {
|
||||
GetAllReverseProxies(ctx context.Context, accountID, userID string) ([]*ReverseProxy, error)
|
||||
GetReverseProxy(ctx context.Context, accountID, userID, reverseProxyID string) (*ReverseProxy, error)
|
||||
CreateReverseProxy(ctx context.Context, accountID, userID string, reverseProxy *ReverseProxy) (*ReverseProxy, error)
|
||||
UpdateReverseProxy(ctx context.Context, accountID, userID string, reverseProxy *ReverseProxy) (*ReverseProxy, error)
|
||||
DeleteReverseProxy(ctx context.Context, accountID, userID, reverseProxyID string) error
|
||||
}
|
||||
161
management/internals/modules/reverseproxy/manager/api.go
Normal file
161
management/internals/modules/reverseproxy/manager/api.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy"
|
||||
nbcontext "github.com/netbirdio/netbird/management/server/context"
|
||||
"github.com/netbirdio/netbird/shared/management/http/api"
|
||||
"github.com/netbirdio/netbird/shared/management/http/util"
|
||||
"github.com/netbirdio/netbird/shared/management/status"
|
||||
)
|
||||
|
||||
type handler struct {
|
||||
manager reverseproxy.Manager
|
||||
}
|
||||
|
||||
func RegisterEndpoints(router *mux.Router, manager reverseproxy.Manager) {
|
||||
h := &handler{
|
||||
manager: manager,
|
||||
}
|
||||
|
||||
router.HandleFunc("/reverse-proxies", h.getAllReverseProxies).Methods("GET", "OPTIONS")
|
||||
router.HandleFunc("/reverse-proxies", h.createReverseProxy).Methods("POST", "OPTIONS")
|
||||
router.HandleFunc("/reverse-proxies/{reverseProxyId}", h.getReverseProxy).Methods("GET", "OPTIONS")
|
||||
router.HandleFunc("/reverse-proxies/{reverseProxyId}", h.updateReverseProxy).Methods("PUT", "OPTIONS")
|
||||
router.HandleFunc("/reverse-proxies/{reverseProxyId}", h.deleteReverseProxy).Methods("DELETE", "OPTIONS")
|
||||
}
|
||||
|
||||
func (h *handler) getAllReverseProxies(w http.ResponseWriter, r *http.Request) {
|
||||
userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
|
||||
if err != nil {
|
||||
util.WriteError(r.Context(), err, w)
|
||||
return
|
||||
}
|
||||
|
||||
allReverseProxies, err := h.manager.GetAllReverseProxies(r.Context(), userAuth.AccountId, userAuth.UserId)
|
||||
if err != nil {
|
||||
util.WriteError(r.Context(), err, w)
|
||||
return
|
||||
}
|
||||
|
||||
apiReverseProxies := make([]*api.ReverseProxy, 0, len(allReverseProxies))
|
||||
for _, reverseProxy := range allReverseProxies {
|
||||
apiReverseProxies = append(apiReverseProxies, reverseProxy.ToAPIResponse())
|
||||
}
|
||||
|
||||
util.WriteJSONObject(r.Context(), w, apiReverseProxies)
|
||||
}
|
||||
|
||||
func (h *handler) createReverseProxy(w http.ResponseWriter, r *http.Request) {
|
||||
userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
|
||||
if err != nil {
|
||||
util.WriteError(r.Context(), err, w)
|
||||
return
|
||||
}
|
||||
|
||||
var req api.PostApiReverseProxyJSONRequestBody
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
util.WriteErrorResponse("couldn't parse JSON request", http.StatusBadRequest, w)
|
||||
return
|
||||
}
|
||||
|
||||
reverseProxy := new(reverseproxy.ReverseProxy)
|
||||
reverseProxy.FromAPIRequest(&req)
|
||||
|
||||
if err = reverseProxy.Validate(); err != nil {
|
||||
util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "%s", err.Error()), w)
|
||||
return
|
||||
}
|
||||
|
||||
createdReverseProxy, err := h.manager.CreateReverseProxy(r.Context(), userAuth.AccountId, userAuth.UserId, reverseProxy)
|
||||
if err != nil {
|
||||
util.WriteError(r.Context(), err, w)
|
||||
return
|
||||
}
|
||||
|
||||
util.WriteJSONObject(r.Context(), w, createdReverseProxy.ToAPIResponse())
|
||||
}
|
||||
|
||||
func (h *handler) getReverseProxy(w http.ResponseWriter, r *http.Request) {
|
||||
userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
|
||||
if err != nil {
|
||||
util.WriteError(r.Context(), err, w)
|
||||
return
|
||||
}
|
||||
|
||||
reverseProxyID := mux.Vars(r)["proxyId"]
|
||||
if reverseProxyID == "" {
|
||||
util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "reverse proxy ID is required"), w)
|
||||
return
|
||||
}
|
||||
|
||||
reverseProxy, err := h.manager.GetReverseProxy(r.Context(), userAuth.AccountId, userAuth.UserId, reverseProxyID)
|
||||
if err != nil {
|
||||
util.WriteError(r.Context(), err, w)
|
||||
return
|
||||
}
|
||||
|
||||
util.WriteJSONObject(r.Context(), w, reverseProxy.ToAPIResponse())
|
||||
}
|
||||
|
||||
func (h *handler) updateReverseProxy(w http.ResponseWriter, r *http.Request) {
|
||||
userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
|
||||
if err != nil {
|
||||
util.WriteError(r.Context(), err, w)
|
||||
return
|
||||
}
|
||||
|
||||
reverseProxyID := mux.Vars(r)["proxyId"]
|
||||
if reverseProxyID == "" {
|
||||
util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "reverse proxy ID is required"), w)
|
||||
return
|
||||
}
|
||||
|
||||
var req api.PutApiReverseProxyProxyIdJSONRequestBody
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
util.WriteErrorResponse("couldn't parse JSON request", http.StatusBadRequest, w)
|
||||
return
|
||||
}
|
||||
|
||||
reverseProxy := new(reverseproxy.ReverseProxy)
|
||||
reverseProxy.ID = reverseProxyID
|
||||
reverseProxy.FromAPIRequest(&req)
|
||||
|
||||
if err = reverseProxy.Validate(); err != nil {
|
||||
util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "%s", err.Error()), w)
|
||||
return
|
||||
}
|
||||
|
||||
updatedReverseProxy, err := h.manager.UpdateReverseProxy(r.Context(), userAuth.AccountId, userAuth.UserId, reverseProxy)
|
||||
if err != nil {
|
||||
util.WriteError(r.Context(), err, w)
|
||||
return
|
||||
}
|
||||
|
||||
util.WriteJSONObject(r.Context(), w, updatedReverseProxy.ToAPIResponse())
|
||||
}
|
||||
|
||||
func (h *handler) deleteReverseProxy(w http.ResponseWriter, r *http.Request) {
|
||||
userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
|
||||
if err != nil {
|
||||
util.WriteError(r.Context(), err, w)
|
||||
return
|
||||
}
|
||||
|
||||
reverseProxyID := mux.Vars(r)["proxyId"]
|
||||
if reverseProxyID == "" {
|
||||
util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "reverse proxy ID is required"), w)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.manager.DeleteReverseProxy(r.Context(), userAuth.AccountId, userAuth.UserId, reverseProxyID); err != nil {
|
||||
util.WriteError(r.Context(), err, w)
|
||||
return
|
||||
}
|
||||
|
||||
util.WriteJSONObject(r.Context(), w, util.EmptyObject{})
|
||||
}
|
||||
171
management/internals/modules/reverseproxy/manager/manager.go
Normal file
171
management/internals/modules/reverseproxy/manager/manager.go
Normal file
@@ -0,0 +1,171 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy"
|
||||
"github.com/netbirdio/netbird/management/server/account"
|
||||
"github.com/netbirdio/netbird/management/server/activity"
|
||||
"github.com/netbirdio/netbird/management/server/permissions"
|
||||
"github.com/netbirdio/netbird/management/server/permissions/modules"
|
||||
"github.com/netbirdio/netbird/management/server/permissions/operations"
|
||||
"github.com/netbirdio/netbird/management/server/store"
|
||||
"github.com/netbirdio/netbird/shared/management/status"
|
||||
)
|
||||
|
||||
type managerImpl struct {
|
||||
store store.Store
|
||||
accountManager account.Manager
|
||||
permissionsManager permissions.Manager
|
||||
}
|
||||
|
||||
func NewManager(store store.Store, accountManager account.Manager, permissionsManager permissions.Manager) reverseproxy.Manager {
|
||||
return &managerImpl{
|
||||
store: store,
|
||||
accountManager: accountManager,
|
||||
permissionsManager: permissionsManager,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *managerImpl) GetAllReverseProxies(ctx context.Context, accountID, userID string) ([]*reverseproxy.ReverseProxy, error) {
|
||||
ok, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Read)
|
||||
if err != nil {
|
||||
return nil, status.NewPermissionValidationError(err)
|
||||
}
|
||||
if !ok {
|
||||
return nil, status.NewPermissionDeniedError()
|
||||
}
|
||||
|
||||
return m.store.GetAccountReverseProxies(ctx, store.LockingStrengthNone, accountID)
|
||||
}
|
||||
|
||||
func (m *managerImpl) GetReverseProxy(ctx context.Context, accountID, userID, reverseProxyID string) (*reverseproxy.ReverseProxy, error) {
|
||||
ok, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Read)
|
||||
if err != nil {
|
||||
return nil, status.NewPermissionValidationError(err)
|
||||
}
|
||||
if !ok {
|
||||
return nil, status.NewPermissionDeniedError()
|
||||
}
|
||||
|
||||
return m.store.GetReverseProxyByID(ctx, store.LockingStrengthNone, accountID, reverseProxyID)
|
||||
}
|
||||
|
||||
func (m *managerImpl) CreateReverseProxy(ctx context.Context, accountID, userID string, reverseProxy *reverseproxy.ReverseProxy) (*reverseproxy.ReverseProxy, error) {
|
||||
ok, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Create)
|
||||
if err != nil {
|
||||
return nil, status.NewPermissionValidationError(err)
|
||||
}
|
||||
if !ok {
|
||||
return nil, status.NewPermissionDeniedError()
|
||||
}
|
||||
|
||||
authConfig := reverseProxy.Auth
|
||||
|
||||
reverseProxy = reverseproxy.NewReverseProxy(accountID, reverseProxy.Name, reverseProxy.Domain, reverseProxy.Targets, reverseProxy.Enabled)
|
||||
|
||||
reverseProxy.Auth = authConfig
|
||||
|
||||
err = m.store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
|
||||
// Check for duplicate domain
|
||||
existingReverseProxy, err := transaction.GetReverseProxyByDomain(ctx, accountID, reverseProxy.Domain)
|
||||
if err != nil {
|
||||
if sErr, ok := status.FromError(err); !ok || sErr.Type() != status.NotFound {
|
||||
return fmt.Errorf("failed to check existing reverse proxy: %w", err)
|
||||
}
|
||||
}
|
||||
if existingReverseProxy != nil {
|
||||
return status.Errorf(status.AlreadyExists, "reverse proxy with domain %s already exists", reverseProxy.Domain)
|
||||
}
|
||||
|
||||
if err = transaction.CreateReverseProxy(ctx, reverseProxy); err != nil {
|
||||
return fmt.Errorf("failed to create reverse proxy: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m.accountManager.StoreEvent(ctx, userID, reverseProxy.ID, accountID, activity.ReverseProxyCreated, reverseProxy.EventMeta())
|
||||
|
||||
return reverseProxy, nil
|
||||
}
|
||||
|
||||
func (m *managerImpl) UpdateReverseProxy(ctx context.Context, accountID, userID string, reverseProxy *reverseproxy.ReverseProxy) (*reverseproxy.ReverseProxy, error) {
|
||||
ok, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Update)
|
||||
if err != nil {
|
||||
return nil, status.NewPermissionValidationError(err)
|
||||
}
|
||||
if !ok {
|
||||
return nil, status.NewPermissionDeniedError()
|
||||
}
|
||||
|
||||
err = m.store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
|
||||
// Get existing reverse proxy
|
||||
existingReverseProxy, err := transaction.GetReverseProxyByID(ctx, store.LockingStrengthUpdate, accountID, reverseProxy.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if domain changed and if it conflicts
|
||||
if existingReverseProxy.Domain != reverseProxy.Domain {
|
||||
conflictReverseProxy, err := transaction.GetReverseProxyByDomain(ctx, accountID, reverseProxy.Domain)
|
||||
if err != nil {
|
||||
if sErr, ok := status.FromError(err); !ok || sErr.Type() != status.NotFound {
|
||||
return fmt.Errorf("failed to check existing reverse proxy: %w", err)
|
||||
}
|
||||
}
|
||||
if conflictReverseProxy != nil && conflictReverseProxy.ID != reverseProxy.ID {
|
||||
return status.Errorf(status.AlreadyExists, "reverse proxy with domain %s already exists", reverseProxy.Domain)
|
||||
}
|
||||
}
|
||||
|
||||
if err = transaction.UpdateReverseProxy(ctx, reverseProxy); err != nil {
|
||||
return fmt.Errorf("failed to update reverse proxy: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m.accountManager.StoreEvent(ctx, userID, reverseProxy.ID, accountID, activity.ReverseProxyUpdated, reverseProxy.EventMeta())
|
||||
|
||||
return reverseProxy, nil
|
||||
}
|
||||
|
||||
func (m *managerImpl) DeleteReverseProxy(ctx context.Context, accountID, userID, reverseProxyID string) error {
|
||||
ok, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Delete)
|
||||
if err != nil {
|
||||
return status.NewPermissionValidationError(err)
|
||||
}
|
||||
if !ok {
|
||||
return status.NewPermissionDeniedError()
|
||||
}
|
||||
|
||||
var reverseProxy *reverseproxy.ReverseProxy
|
||||
err = m.store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
|
||||
var err error
|
||||
reverseProxy, err = transaction.GetReverseProxyByID(ctx, store.LockingStrengthUpdate, accountID, reverseProxyID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = transaction.DeleteReverseProxy(ctx, accountID, reverseProxyID); err != nil {
|
||||
return fmt.Errorf("failed to delete reverse proxy: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.accountManager.StoreEvent(ctx, userID, reverseProxyID, accountID, activity.ReverseProxyDeleted, reverseProxy.EventMeta())
|
||||
|
||||
return nil
|
||||
}
|
||||
196
management/internals/modules/reverseproxy/reverseproxy.go
Normal file
196
management/internals/modules/reverseproxy/reverseproxy.go
Normal file
@@ -0,0 +1,196 @@
|
||||
package reverseproxy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/rs/xid"
|
||||
|
||||
"github.com/netbirdio/netbird/shared/management/http/api"
|
||||
)
|
||||
|
||||
type Target struct {
|
||||
Path *string `json:"path,omitempty"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Protocol string `json:"protocol"`
|
||||
TargetId string `json:"target_id"`
|
||||
TargetType string `json:"target_type"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
type PasswordAuthConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type PINAuthConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Pin string `json:"pin"`
|
||||
}
|
||||
|
||||
type BearerAuthConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
DistributionGroups []string `json:"distribution_groups,omitempty" gorm:"serializer:json"`
|
||||
}
|
||||
|
||||
type LinkAuthConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
type AuthConfig struct {
|
||||
PasswordAuth *PasswordAuthConfig `json:"password_auth,omitempty" gorm:"serializer:json"`
|
||||
PinAuth *PINAuthConfig `json:"pin_auth,omitempty" gorm:"serializer:json"`
|
||||
BearerAuth *BearerAuthConfig `json:"bearer_auth,omitempty" gorm:"serializer:json"`
|
||||
LinkAuth *LinkAuthConfig `json:"link_auth,omitempty" gorm:"serializer:json"`
|
||||
}
|
||||
|
||||
type ReverseProxy struct {
|
||||
ID string `gorm:"primaryKey"`
|
||||
AccountID string `gorm:"index"`
|
||||
Name string
|
||||
Domain string `gorm:"index"`
|
||||
Targets []Target `gorm:"serializer:json"`
|
||||
Enabled bool
|
||||
Auth AuthConfig `gorm:"serializer:json"`
|
||||
}
|
||||
|
||||
func NewReverseProxy(accountID, name, domain string, targets []Target, enabled bool) *ReverseProxy {
|
||||
return &ReverseProxy{
|
||||
ID: xid.New().String(),
|
||||
AccountID: accountID,
|
||||
Name: name,
|
||||
Domain: domain,
|
||||
Targets: targets,
|
||||
Enabled: enabled,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ReverseProxy) ToAPIResponse() *api.ReverseProxy {
|
||||
authConfig := api.ReverseProxyAuthConfig{}
|
||||
|
||||
if r.Auth.PasswordAuth != nil {
|
||||
authConfig.PasswordAuth = &api.PasswordAuthConfig{
|
||||
Enabled: r.Auth.PasswordAuth.Enabled,
|
||||
Password: r.Auth.PasswordAuth.Password,
|
||||
}
|
||||
}
|
||||
|
||||
if r.Auth.PinAuth != nil {
|
||||
authConfig.PinAuth = &api.PINAuthConfig{
|
||||
Enabled: r.Auth.PinAuth.Enabled,
|
||||
Pin: r.Auth.PinAuth.Pin,
|
||||
}
|
||||
}
|
||||
|
||||
if r.Auth.BearerAuth != nil {
|
||||
authConfig.BearerAuth = &api.BearerAuthConfig{
|
||||
Enabled: r.Auth.BearerAuth.Enabled,
|
||||
DistributionGroups: &r.Auth.BearerAuth.DistributionGroups,
|
||||
}
|
||||
}
|
||||
|
||||
if r.Auth.LinkAuth != nil {
|
||||
authConfig.LinkAuth = &api.LinkAuthConfig{
|
||||
Enabled: r.Auth.LinkAuth.Enabled,
|
||||
}
|
||||
}
|
||||
|
||||
// Convert internal targets to API targets
|
||||
apiTargets := make([]api.ReverseProxyTarget, 0, len(r.Targets))
|
||||
for _, target := range r.Targets {
|
||||
apiTargets = append(apiTargets, api.ReverseProxyTarget{
|
||||
Path: target.Path,
|
||||
Host: target.Host,
|
||||
Port: target.Port,
|
||||
Protocol: api.ReverseProxyTargetProtocol(target.Protocol),
|
||||
TargetId: target.TargetId,
|
||||
TargetType: api.ReverseProxyTargetTargetType(target.TargetType),
|
||||
Enabled: target.Enabled,
|
||||
})
|
||||
}
|
||||
|
||||
return &api.ReverseProxy{
|
||||
Id: r.ID,
|
||||
Name: r.Name,
|
||||
Domain: r.Domain,
|
||||
Targets: apiTargets,
|
||||
Enabled: r.Enabled,
|
||||
Auth: authConfig,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ReverseProxy) FromAPIRequest(req *api.ReverseProxyRequest) {
|
||||
r.Name = req.Name
|
||||
r.Domain = req.Domain
|
||||
|
||||
// Convert API targets to internal targets
|
||||
targets := make([]Target, 0, len(req.Targets))
|
||||
for _, apiTarget := range req.Targets {
|
||||
targets = append(targets, Target{
|
||||
Path: apiTarget.Path,
|
||||
Host: apiTarget.Host,
|
||||
Port: apiTarget.Port,
|
||||
Protocol: string(apiTarget.Protocol),
|
||||
TargetId: apiTarget.TargetId,
|
||||
TargetType: string(apiTarget.TargetType),
|
||||
Enabled: apiTarget.Enabled,
|
||||
})
|
||||
}
|
||||
r.Targets = targets
|
||||
|
||||
r.Enabled = req.Enabled
|
||||
|
||||
if req.Auth.PasswordAuth != nil {
|
||||
r.Auth.PasswordAuth = &PasswordAuthConfig{
|
||||
Enabled: req.Auth.PasswordAuth.Enabled,
|
||||
Password: req.Auth.PasswordAuth.Password,
|
||||
}
|
||||
}
|
||||
|
||||
if req.Auth.PinAuth != nil {
|
||||
r.Auth.PinAuth = &PINAuthConfig{
|
||||
Enabled: req.Auth.PinAuth.Enabled,
|
||||
Pin: req.Auth.PinAuth.Pin,
|
||||
}
|
||||
}
|
||||
|
||||
if req.Auth.BearerAuth != nil {
|
||||
bearerAuth := &BearerAuthConfig{
|
||||
Enabled: req.Auth.BearerAuth.Enabled,
|
||||
}
|
||||
if req.Auth.BearerAuth.DistributionGroups != nil {
|
||||
bearerAuth.DistributionGroups = *req.Auth.BearerAuth.DistributionGroups
|
||||
}
|
||||
r.Auth.BearerAuth = bearerAuth
|
||||
}
|
||||
|
||||
if req.Auth.LinkAuth != nil {
|
||||
r.Auth.LinkAuth = &LinkAuthConfig{
|
||||
Enabled: req.Auth.LinkAuth.Enabled,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (r *ReverseProxy) Validate() error {
|
||||
if r.Name == "" {
|
||||
return errors.New("reverse proxy name is required")
|
||||
}
|
||||
if len(r.Name) > 255 {
|
||||
return errors.New("reverse proxy name exceeds maximum length of 255 characters")
|
||||
}
|
||||
|
||||
if r.Domain == "" {
|
||||
return errors.New("reverse proxy domain is required")
|
||||
}
|
||||
|
||||
if len(r.Targets) == 0 {
|
||||
return errors.New("at least one target is required")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ReverseProxy) EventMeta() map[string]any {
|
||||
return map[string]any{"name": r.Name, "domain": r.Domain}
|
||||
}
|
||||
Reference in New Issue
Block a user