mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-08-29 13:40:42 +02:00
change access directive and add management for proxy allow list
Signed-off-by: Ronni Skansing <rskansing@gmail.com>
This commit is contained in:
@@ -94,6 +94,9 @@ const (
|
||||
ROUTE_V1_PROXY = "/api/v1/proxy"
|
||||
ROUTE_V1_PROXY_OVERVIEW = "/api/v1/proxy/overview"
|
||||
ROUTE_V1_PROXY_ID = "/api/v1/proxy/:id"
|
||||
// ip allow list
|
||||
ROUTE_V1_IP_ALLOW_LIST_PROXY_CONFIG = "/api/v1/ip-allow-list/proxy-config/:id"
|
||||
ROUTE_V1_IP_ALLOW_LIST_CLEAR_PROXY_CONFIG = "/api/v1/ip-allow-list/clear-proxy-config/:id"
|
||||
// recipient and groups
|
||||
ROUTE_V1_RECIPIENT = "/api/v1/recipient"
|
||||
ROUTE_V1_RECIPIENT_IMPORT = "/api/v1/recipient/import"
|
||||
@@ -339,6 +342,9 @@ func setupRoutes(
|
||||
POST(ROUTE_V1_PROXY, middleware.SessionHandler, controllers.Proxy.Create).
|
||||
PATCH(ROUTE_V1_PROXY_ID, middleware.SessionHandler, controllers.Proxy.UpdateByID).
|
||||
DELETE(ROUTE_V1_PROXY_ID, middleware.SessionHandler, controllers.Proxy.DeleteByID).
|
||||
// ip allow list
|
||||
GET(ROUTE_V1_IP_ALLOW_LIST_PROXY_CONFIG, middleware.SessionHandler, controllers.IPAllowList.GetEntriesForProxyConfig).
|
||||
DELETE(ROUTE_V1_IP_ALLOW_LIST_CLEAR_PROXY_CONFIG, middleware.SessionHandler, controllers.IPAllowList.ClearForProxyConfig).
|
||||
// smtp configuration
|
||||
GET(ROUTE_V1_SMTP_CONFIGURATION, middleware.SessionHandler, controllers.SMTPConfiguration.GetAll).
|
||||
GET(ROUTE_V1_SMTP_CONFIGURATION_ID, middleware.SessionHandler, controllers.SMTPConfiguration.GetByID).
|
||||
|
||||
@@ -36,6 +36,7 @@ type Controllers struct {
|
||||
Update *controller.Update
|
||||
Import *controller.Import
|
||||
Backup *controller.Backup
|
||||
IPAllowList *controller.IPAllowList
|
||||
}
|
||||
|
||||
// NewControllers creates a collection of controllers
|
||||
@@ -179,6 +180,10 @@ func NewControllers(
|
||||
Common: common,
|
||||
BackupService: services.Backup,
|
||||
}
|
||||
ipAllowList := &controller.IPAllowList{
|
||||
Common: common,
|
||||
IPAllowListService: services.IPAllowList,
|
||||
}
|
||||
|
||||
return &Controllers{
|
||||
Asset: asset,
|
||||
@@ -209,5 +214,6 @@ func NewControllers(
|
||||
Update: update,
|
||||
Import: importController,
|
||||
Backup: backup,
|
||||
IPAllowList: ipAllowList,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,12 +67,6 @@ func NewServer(
|
||||
logger *zap.SugaredLogger,
|
||||
certMagicConfig *certmagic.Config,
|
||||
) *Server {
|
||||
// setup proxy cookie tracking
|
||||
cookieName := ""
|
||||
if option, err := repositories.Option.GetByKey(context.Background(), data.OptionKeyProxyCookieName); err == nil && option != nil {
|
||||
cookieName = option.Value.String()
|
||||
}
|
||||
|
||||
// setup goproxy-based proxy server
|
||||
proxyServer := proxy.NewProxyHandler(
|
||||
logger,
|
||||
@@ -85,7 +79,7 @@ func NewServer(
|
||||
repositories.Identifier,
|
||||
services.Campaign,
|
||||
services.Template,
|
||||
cookieName,
|
||||
services.IPAllowList,
|
||||
)
|
||||
|
||||
// setup proxy session cleanup routine
|
||||
|
||||
@@ -36,6 +36,7 @@ type Services struct {
|
||||
Update *service.Update
|
||||
Import *service.Import
|
||||
Backup *service.Backup
|
||||
IPAllowList *service.IPAllowListService
|
||||
}
|
||||
|
||||
// NewServices creates a collection of services
|
||||
@@ -164,6 +165,7 @@ func NewServices(
|
||||
CampaignTemplateService: campaignTemplate,
|
||||
DomainService: domain,
|
||||
}
|
||||
ipAllowListService := service.NewIPAllowListService(logger, repositories.Proxy)
|
||||
email := &service.Email{
|
||||
Common: common,
|
||||
AttachmentPath: attachmentPath,
|
||||
@@ -272,5 +274,6 @@ func NewServices(
|
||||
Update: updateService,
|
||||
Import: importService,
|
||||
Backup: backupService,
|
||||
IPAllowList: ipAllowListService,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/phishingclub/phishingclub/service"
|
||||
)
|
||||
|
||||
// IPAllowList is the controller for IP allow list management
|
||||
type IPAllowList struct {
|
||||
Common
|
||||
IPAllowListService *service.IPAllowListService
|
||||
}
|
||||
|
||||
// GetEntriesForProxyConfig returns IP allow list entries for a specific proxy configuration
|
||||
func (c *IPAllowList) GetEntriesForProxyConfig(g *gin.Context) {
|
||||
// handle session
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// parse proxy config ID from URL params
|
||||
proxyConfigID, ok := c.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// get entries for proxy config
|
||||
entries, err := c.IPAllowListService.GetEntriesForProxyConfig(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
proxyConfigID,
|
||||
)
|
||||
|
||||
// handle response
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(g, entries)
|
||||
}
|
||||
|
||||
// ClearForProxyConfig removes all entries for a specific proxy configuration
|
||||
func (c *IPAllowList) ClearForProxyConfig(g *gin.Context) {
|
||||
// handle session
|
||||
session, _, ok := c.handleSession(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// parse proxy config ID from URL params
|
||||
proxyConfigID, ok := c.handleParseIDParam(g)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// clear entries for proxy config
|
||||
count, err := c.IPAllowListService.ClearForProxyConfig(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
proxyConfigID,
|
||||
)
|
||||
|
||||
// handle response
|
||||
if ok := c.handleErrors(g, err); !ok {
|
||||
return
|
||||
}
|
||||
c.Response.OK(g, map[string]interface{}{
|
||||
"message": "Entries cleared for proxy configuration",
|
||||
"cleared_count": count,
|
||||
})
|
||||
}
|
||||
@@ -31,7 +31,7 @@ type CampaignTemplate struct {
|
||||
|
||||
// landing page can also be a proxy
|
||||
LandingProxyID *uuid.UUID `gorm:"type:uuid;index;"`
|
||||
LandingProxy *Proxy `gorm:"foreignKey:LandingProxyID;references:ID;"`
|
||||
LandingProxy *Proxy `gorm:"foreignKey:LandingProxyID;references:ID;"`
|
||||
|
||||
DomainID *uuid.UUID `gorm:"type:uuid;index;"`
|
||||
Domain *Domain `gorm:"foreignKey:DomainID"`
|
||||
@@ -48,16 +48,16 @@ type CampaignTemplate struct {
|
||||
|
||||
// before landing page can also be a proxy
|
||||
BeforeLandingProxyID *uuid.UUID `gorm:"type:uuid;index"`
|
||||
BeforeLandingProxy *Proxy `gorm:"foreignKey:BeforeLandingProxyID;references:ID"`
|
||||
BeforeLandingProxy *Proxy `gorm:"foreignKey:BeforeLandingProxyID;references:ID"`
|
||||
|
||||
AfterLandingPageID *uuid.UUID `gorm:"type:uuid;index"`
|
||||
AfterLandingPage *Page `gorm:"foreignKey:AfterLandingPageID;references:ID"`
|
||||
|
||||
// after landing page can also be a proxy
|
||||
AfterLandingProxyID *uuid.UUID `gorm:"type:uuid;index"`
|
||||
AfterLandingProxy *Proxy `gorm:"foreignKey:AfterLandingProxyID;references:ID"`
|
||||
AfterLandingProxy *Proxy `gorm:"foreignKey:AfterLandingProxyID;references:ID"`
|
||||
|
||||
AfterLandingPageRedirectURL string `gorm:"not null;"`
|
||||
AfterLandingPageRedirectURL string `gorm:"not null;default:'';"`
|
||||
|
||||
EmailID *uuid.UUID `gorm:"type:uuid;index;"`
|
||||
Email *Email `gorm:"foreignKey:EmailID;references:ID;"`
|
||||
|
||||
@@ -89,9 +89,7 @@ func (c *CampaignTemplate) Validate() error {
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := validate.NullableFieldRequired("urlPath", c.URLPath); err != nil {
|
||||
return err
|
||||
}
|
||||
// URLPath is optional, no validation needed
|
||||
|
||||
// validate that only one type is set per stage
|
||||
// before landing page: can have neither (optional), or one type, but not both
|
||||
@@ -198,9 +196,13 @@ func (c *CampaignTemplate) ToDBMap() map[string]any {
|
||||
}
|
||||
if c.AfterLandingPageRedirectURL.IsSpecified() {
|
||||
if c.AfterLandingPageRedirectURL.IsNull() {
|
||||
m["after_landing_page_redirect_url"] = nil
|
||||
m["after_landing_page_redirect_url"] = ""
|
||||
} else {
|
||||
m["after_landing_page_redirect_url"] = c.AfterLandingPageRedirectURL.MustGet().String()
|
||||
if v, err := c.AfterLandingPageRedirectURL.Get(); err == nil {
|
||||
m["after_landing_page_redirect_url"] = v.String()
|
||||
} else {
|
||||
m["after_landing_page_redirect_url"] = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,8 +241,16 @@ func (c *CampaignTemplate) ToDBMap() map[string]any {
|
||||
if v, err := c.StateIdentifierID.Get(); err == nil {
|
||||
m["state_identifier_id"] = v
|
||||
}
|
||||
if v, err := c.URLPath.Get(); err == nil {
|
||||
m["url_path"] = v.String()
|
||||
if c.URLPath.IsSpecified() {
|
||||
if c.URLPath.IsNull() {
|
||||
m["url_path"] = ""
|
||||
} else {
|
||||
if v, err := c.URLPath.Get(); err == nil {
|
||||
m["url_path"] = v.String()
|
||||
} else {
|
||||
m["url_path"] = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_, errDomain := c.DomainID.Get()
|
||||
|
||||
+35
-93
@@ -115,36 +115,36 @@ type ProxyHandler struct {
|
||||
IdentifierRepository *repository.Identifier
|
||||
CampaignService *service.Campaign
|
||||
TemplateService *service.Template
|
||||
IPAllowListService *service.IPAllowListService
|
||||
cookieName string
|
||||
ipAllowList sync.Map // map[string]int64 (ip+domain -> expiry timestamp)
|
||||
}
|
||||
|
||||
func NewProxyHandler(
|
||||
logger *zap.SugaredLogger,
|
||||
pageRepository *repository.Page,
|
||||
campaignRecipientRepository *repository.CampaignRecipient,
|
||||
campaignRepository *repository.Campaign,
|
||||
campaignTemplateRepository *repository.CampaignTemplate,
|
||||
domainRepository *repository.Domain,
|
||||
proxyRepository *repository.Proxy,
|
||||
identifierRepository *repository.Identifier,
|
||||
pageRepo *repository.Page,
|
||||
campaignRecipientRepo *repository.CampaignRecipient,
|
||||
campaignRepo *repository.Campaign,
|
||||
campaignTemplateRepo *repository.CampaignTemplate,
|
||||
domainRepo *repository.Domain,
|
||||
proxyRepo *repository.Proxy,
|
||||
identifierRepo *repository.Identifier,
|
||||
campaignService *service.Campaign,
|
||||
templateService *service.Template,
|
||||
cookieName string,
|
||||
ipAllowListService *service.IPAllowListService,
|
||||
) *ProxyHandler {
|
||||
return &ProxyHandler{
|
||||
logger: logger,
|
||||
sessions: sync.Map{},
|
||||
PageRepository: pageRepository,
|
||||
CampaignRecipientRepository: campaignRecipientRepository,
|
||||
CampaignRepository: campaignRepository,
|
||||
CampaignTemplateRepository: campaignTemplateRepository,
|
||||
DomainRepository: domainRepository,
|
||||
ProxyRepository: proxyRepository,
|
||||
IdentifierRepository: identifierRepository,
|
||||
PageRepository: pageRepo,
|
||||
CampaignRecipientRepository: campaignRecipientRepo,
|
||||
CampaignRepository: campaignRepo,
|
||||
CampaignTemplateRepository: campaignTemplateRepo,
|
||||
DomainRepository: domainRepo,
|
||||
ProxyRepository: proxyRepo,
|
||||
IdentifierRepository: identifierRepo,
|
||||
CampaignService: campaignService,
|
||||
TemplateService: templateService,
|
||||
cookieName: cookieName,
|
||||
IPAllowListService: ipAllowListService,
|
||||
cookieName: "ps",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,7 +364,10 @@ func (m *ProxyHandler) resolveSessionContext(req *http.Request, reqCtx *RequestC
|
||||
m.registerPageVisitEvent(req, newSession)
|
||||
|
||||
// allow list IP for tunnel mode access
|
||||
m.allowListIP(req, reqCtx.Domain.Name)
|
||||
clientIP := m.getClientIP(req)
|
||||
if clientIP != "" {
|
||||
m.IPAllowListService.AddIP(clientIP, reqCtx.Domain.ProxyID.String(), 10*time.Minute)
|
||||
}
|
||||
} else {
|
||||
// load existing session
|
||||
sessionVal, exists := m.sessions.Load(reqCtx.SessionID)
|
||||
@@ -2401,24 +2404,7 @@ func (m *ProxyHandler) CleanupExpiredSessions() {
|
||||
}
|
||||
|
||||
// cleanup expired IP allow listed entries
|
||||
ipCleanedCount := 0
|
||||
currentTime := now.Unix()
|
||||
|
||||
m.ipAllowList.Range(func(key, value interface{}) bool {
|
||||
expiry, ok := value.(int64)
|
||||
if !ok {
|
||||
m.ipAllowList.Delete(key)
|
||||
ipCleanedCount++
|
||||
return true
|
||||
}
|
||||
|
||||
if currentTime >= expiry {
|
||||
m.ipAllowList.Delete(key)
|
||||
ipCleanedCount++
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
ipCleanedCount := m.IPAllowListService.ClearExpired()
|
||||
if ipCleanedCount > 0 {
|
||||
m.logger.Debugw("cleaned up expired IP allow listed entries", "count", ipCleanedCount)
|
||||
}
|
||||
@@ -2561,9 +2547,12 @@ func (m *ProxyHandler) checkAccessRules(path string, accessControl *service.Prox
|
||||
return true, ""
|
||||
}
|
||||
|
||||
// check if IP is allowlisted for this domain (from previous lure access)
|
||||
if reqCtx != nil && reqCtx.Domain != nil && req != nil && m.isIPAllowlistedForRequest(req, reqCtx.Domain.Name) {
|
||||
return true, ""
|
||||
// check if IP is allowlisted for this proxy config (from previous lure access)
|
||||
if reqCtx != nil && reqCtx.Domain != nil && req != nil {
|
||||
clientIP := m.getClientIP(req)
|
||||
if clientIP != "" && m.IPAllowListService.IsIPAllowed(clientIP, reqCtx.Domain.ProxyID.String()) {
|
||||
return true, ""
|
||||
}
|
||||
}
|
||||
|
||||
// no lure request and IP not allow listed - deny access
|
||||
@@ -2580,65 +2569,18 @@ func (m *ProxyHandler) applyDefaultPrivateMode(reqCtx *RequestContext, req *http
|
||||
return true, ""
|
||||
}
|
||||
|
||||
// check if IP is allow listed for this domain (from previous lure access)
|
||||
if reqCtx != nil && reqCtx.Domain != nil && req != nil && m.isIPAllowlistedForRequest(req, reqCtx.Domain.Name) {
|
||||
return true, ""
|
||||
// check if IP is allowlisted for this proxy config (from previous lure access)
|
||||
if reqCtx != nil && reqCtx.Domain != nil && req != nil {
|
||||
clientIP := m.getClientIP(req)
|
||||
if clientIP != "" && m.IPAllowListService.IsIPAllowed(clientIP, reqCtx.Domain.ProxyID.String()) {
|
||||
return true, ""
|
||||
}
|
||||
}
|
||||
|
||||
// no lure request and IP not allow listed - deny with default action
|
||||
return false, "404"
|
||||
}
|
||||
|
||||
// allowListIP adds an IP address to the allow list for private mode access
|
||||
func (m *ProxyHandler) allowListIP(req *http.Request, domain string) {
|
||||
clientIP := m.getClientIP(req)
|
||||
if clientIP == "" {
|
||||
return
|
||||
}
|
||||
|
||||
key := clientIP + "-" + domain
|
||||
// allowlisted for 10 minutes
|
||||
expiry := time.Now().Add(10 * time.Minute).Unix()
|
||||
|
||||
m.ipAllowList.Store(key, expiry)
|
||||
|
||||
m.logger.Debugw("IP allow listed for private mode",
|
||||
"ip", clientIP,
|
||||
"domain", domain,
|
||||
"expires_at", time.Unix(expiry, 0).Format(time.RFC3339),
|
||||
)
|
||||
}
|
||||
|
||||
// isIPAllowlistedForRequest checks if an IP is allowlisted for a specific domain
|
||||
func (m *ProxyHandler) isIPAllowlistedForRequest(req *http.Request, domain string) bool {
|
||||
clientIP := m.getClientIP(req)
|
||||
if clientIP == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
key := clientIP + "-" + domain
|
||||
|
||||
if expiryVal, exists := m.ipAllowList.Load(key); exists {
|
||||
expiry := expiryVal.(int64)
|
||||
if time.Now().Unix() < expiry {
|
||||
m.logger.Debugw("IP found in allow list for private mode",
|
||||
"ip", clientIP,
|
||||
"domain", domain,
|
||||
"expires_at", time.Unix(expiry, 0).Format(time.RFC3339),
|
||||
)
|
||||
return true
|
||||
}
|
||||
// expired, remove it
|
||||
m.ipAllowList.Delete(key)
|
||||
m.logger.Debugw("IP allow listed entry expired and removed",
|
||||
"ip", clientIP,
|
||||
"domain", domain,
|
||||
)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// getClientIP extracts the real client IP from request headers
|
||||
func (m *ProxyHandler) getClientIP(req *http.Request) string {
|
||||
// check common proxy headers first
|
||||
|
||||
@@ -85,6 +85,10 @@ func (c *CampaignTemplate) Create(
|
||||
if !campaignTemplate.URLPath.IsSpecified() || campaignTemplate.URLPath.IsNull() {
|
||||
campaignTemplate.URLPath = nullable.NewNullableWithValue(*vo.NewURLPathMust(""))
|
||||
}
|
||||
// if no afterLandingPageRedirectURL set to ''
|
||||
if !campaignTemplate.AfterLandingPageRedirectURL.IsSpecified() || campaignTemplate.AfterLandingPageRedirectURL.IsNull() {
|
||||
campaignTemplate.AfterLandingPageRedirectURL = nullable.NewNullableWithValue(*vo.NewOptionalString255Must(""))
|
||||
}
|
||||
// validate
|
||||
if err := campaignTemplate.Validate(); err != nil {
|
||||
c.Logger.Errorw("failed to validate campaign template", "error", err)
|
||||
@@ -636,7 +640,8 @@ func (c *CampaignTemplate) UpdateByID(
|
||||
if v, err := campaignTemplate.AfterLandingPageRedirectURL.Get(); err == nil {
|
||||
incoming.AfterLandingPageRedirectURL.Set(v)
|
||||
} else {
|
||||
incoming.AfterLandingPageRedirectURL.SetNull()
|
||||
// if AfterLandingPageRedirectURL is null, set to empty string
|
||||
incoming.AfterLandingPageRedirectURL.Set(*vo.NewOptionalString255Must(""))
|
||||
}
|
||||
}
|
||||
if v, err := campaignTemplate.URLIdentifierID.Get(); err == nil {
|
||||
@@ -645,8 +650,13 @@ func (c *CampaignTemplate) UpdateByID(
|
||||
if v, err := campaignTemplate.StateIdentifierID.Get(); err == nil {
|
||||
incoming.StateIdentifierID.Set(v)
|
||||
}
|
||||
if v, err := campaignTemplate.URLPath.Get(); err == nil {
|
||||
incoming.URLPath.Set(v)
|
||||
if campaignTemplate.URLPath.IsSpecified() {
|
||||
if v, err := campaignTemplate.URLPath.Get(); err == nil {
|
||||
incoming.URLPath.Set(v)
|
||||
} else {
|
||||
// if URLPath is null, set to empty string
|
||||
incoming.URLPath.Set(*vo.NewURLPathMust(""))
|
||||
}
|
||||
}
|
||||
// validate
|
||||
if err := incoming.Validate(); err != nil {
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/google/uuid"
|
||||
"github.com/phishingclub/phishingclub/data"
|
||||
"github.com/phishingclub/phishingclub/errs"
|
||||
"github.com/phishingclub/phishingclub/model"
|
||||
"github.com/phishingclub/phishingclub/repository"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// IPAllowListEntry represents a single IP allow list entry
|
||||
type IPAllowListEntry struct {
|
||||
IP string `json:"ip"`
|
||||
ProxyConfigID string `json:"proxyConfigID"`
|
||||
ExpiresAt time.Time `json:"expiresAt"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
// IPAllowListService manages IP allow listing for proxy configurations
|
||||
type IPAllowListService struct {
|
||||
Common
|
||||
logger *zap.SugaredLogger
|
||||
allowList sync.Map // map[string]int64 (ip+proxyConfigID -> expiry timestamp)
|
||||
mu sync.RWMutex
|
||||
cleanupDone chan bool
|
||||
ProxyRepository *repository.Proxy
|
||||
}
|
||||
|
||||
// NewIPAllowListService creates a new IP allow list service
|
||||
func NewIPAllowListService(logger *zap.SugaredLogger, proxyRepo *repository.Proxy) *IPAllowListService {
|
||||
common := Common{
|
||||
Logger: logger,
|
||||
}
|
||||
service := &IPAllowListService{
|
||||
Common: common,
|
||||
logger: logger,
|
||||
cleanupDone: make(chan bool),
|
||||
ProxyRepository: proxyRepo,
|
||||
}
|
||||
|
||||
// Start cleanup goroutine
|
||||
go service.periodicCleanup()
|
||||
|
||||
return service
|
||||
}
|
||||
|
||||
// AddIP adds an IP to the allow list for a specific proxy configuration
|
||||
// must only be called internally and not exposed via. API
|
||||
func (s *IPAllowListService) AddIP(ip string, proxyConfigID string, duration time.Duration) {
|
||||
if ip == "" || proxyConfigID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
key := ip + "-" + proxyConfigID
|
||||
expiry := time.Now().Add(duration).Unix()
|
||||
|
||||
s.allowList.Store(key, expiry)
|
||||
|
||||
s.logger.Debugw("IP allow listed",
|
||||
"ip", ip,
|
||||
"proxy_config_id", proxyConfigID,
|
||||
"expires_at", time.Unix(expiry, 0).Format(time.RFC3339),
|
||||
)
|
||||
}
|
||||
|
||||
// IsIPAllowed checks if an IP is allowed for a specific proxy configuration
|
||||
// must only be called internally and not exposed via. API
|
||||
func (s *IPAllowListService) IsIPAllowed(ip string, proxyConfigID string) bool {
|
||||
if ip == "" || proxyConfigID == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
key := ip + "-" + proxyConfigID
|
||||
|
||||
if expiryVal, exists := s.allowList.Load(key); exists {
|
||||
expiry := expiryVal.(int64)
|
||||
if time.Now().Unix() < expiry {
|
||||
s.logger.Debugw("IP found in allow list",
|
||||
"ip", ip,
|
||||
"proxy_config_id", proxyConfigID,
|
||||
"expires_at", time.Unix(expiry, 0).Format(time.RFC3339),
|
||||
)
|
||||
return true
|
||||
}
|
||||
// Expired, remove it
|
||||
s.allowList.Delete(key)
|
||||
s.logger.Debugw("IP allow list entry expired and removed",
|
||||
"ip", ip,
|
||||
"proxy_config_id", proxyConfigID,
|
||||
)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// GetEntriesForProxyConfig returns allow list entries for a specific proxy configuration
|
||||
func (s *IPAllowListService) GetEntriesForProxyConfig(
|
||||
ctx context.Context,
|
||||
session *model.Session,
|
||||
proxyConfigID *uuid.UUID,
|
||||
) ([]IPAllowListEntry, error) {
|
||||
ae := NewAuditEvent("IPAllowList.GetEntriesForProxyConfig", session)
|
||||
|
||||
// check permissions
|
||||
isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL)
|
||||
if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) {
|
||||
s.LogAuthError(err)
|
||||
return nil, errs.Wrap(err)
|
||||
}
|
||||
if !isAuthorized {
|
||||
s.AuditLogNotAuthorized(ae)
|
||||
return nil, errs.ErrAuthorizationFailed
|
||||
}
|
||||
|
||||
var entries []IPAllowListEntry
|
||||
now := time.Now()
|
||||
proxyConfigIDStr := proxyConfigID.String()
|
||||
|
||||
s.allowList.Range(func(key, value interface{}) bool {
|
||||
keyStr := key.(string)
|
||||
expiry := value.(int64)
|
||||
expiryTime := time.Unix(expiry, 0)
|
||||
|
||||
// Skip expired entries
|
||||
if now.Unix() >= expiry {
|
||||
s.allowList.Delete(key)
|
||||
return true
|
||||
}
|
||||
|
||||
// Parse key to extract IP and proxy config ID
|
||||
parts := parseAllowListKey(keyStr)
|
||||
if len(parts) == 2 && parts[1] == proxyConfigIDStr {
|
||||
entries = append(entries, IPAllowListEntry{
|
||||
IP: parts[0],
|
||||
ProxyConfigID: parts[1],
|
||||
ExpiresAt: expiryTime,
|
||||
CreatedAt: expiryTime.Add(-10 * time.Minute), // Assume 10 minute duration
|
||||
})
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
ae.Details["proxy_config_id"] = proxyConfigID.String()
|
||||
if userId, err := session.User.ID.Get(); err == nil {
|
||||
ae.Details["user_id"] = userId.String()
|
||||
}
|
||||
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
// ClearExpired removes all expired entries from the allow list
|
||||
func (s *IPAllowListService) ClearExpired() int {
|
||||
count := 0
|
||||
now := time.Now().Unix()
|
||||
|
||||
s.allowList.Range(func(key, value interface{}) bool {
|
||||
expiry := value.(int64)
|
||||
if now >= expiry {
|
||||
s.allowList.Delete(key)
|
||||
count++
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
if count > 0 {
|
||||
s.logger.Debugw("Cleaned up expired IP allow list entries", "count", count)
|
||||
}
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
// ClearForProxyConfig removes all entries for a specific proxy configuration
|
||||
func (s *IPAllowListService) ClearForProxyConfig(
|
||||
ctx context.Context,
|
||||
session *model.Session,
|
||||
proxyConfigID *uuid.UUID,
|
||||
) (int, error) {
|
||||
ae := NewAuditEvent("IPAllowList.ClearForProxyConfig", session)
|
||||
|
||||
// check permissions
|
||||
isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL)
|
||||
if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) {
|
||||
s.LogAuthError(err)
|
||||
return 0, errs.Wrap(err)
|
||||
}
|
||||
if !isAuthorized {
|
||||
s.AuditLogNotAuthorized(ae)
|
||||
return 0, errs.ErrAuthorizationFailed
|
||||
}
|
||||
count := 0
|
||||
proxyConfigIDStr := proxyConfigID.String()
|
||||
|
||||
s.allowList.Range(func(key, value interface{}) bool {
|
||||
keyStr := key.(string)
|
||||
parts := parseAllowListKey(keyStr)
|
||||
if len(parts) == 2 && parts[1] == proxyConfigIDStr {
|
||||
s.allowList.Delete(key)
|
||||
count++
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
ae.Details["proxy_config_id"] = proxyConfigID.String()
|
||||
if userId, err := session.User.ID.Get(); err == nil {
|
||||
ae.Details["user_id"] = userId.String()
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// periodicCleanup runs periodic cleanup of expired entries
|
||||
func (s *IPAllowListService) periodicCleanup() {
|
||||
ticker := time.NewTicker(5 * time.Minute) // Clean up every 5 minutes
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
s.ClearExpired()
|
||||
case <-s.cleanupDone:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop stops the background cleanup goroutine
|
||||
func (s *IPAllowListService) Stop() {
|
||||
close(s.cleanupDone)
|
||||
}
|
||||
|
||||
// parseAllowListKey parses the allow list key format "ip-proxyConfigID"
|
||||
func parseAllowListKey(key string) []string {
|
||||
// Find the last occurrence of "-" to handle IPv6 addresses
|
||||
lastIndex := -1
|
||||
for i := len(key) - 1; i >= 0; i-- {
|
||||
if key[i] == '-' {
|
||||
// Check if this looks like a UUID separator (36 chars after this point)
|
||||
remaining := key[i+1:]
|
||||
if len(remaining) == 36 {
|
||||
// Validate it looks like a UUID
|
||||
if _, err := uuid.Parse(remaining); err == nil {
|
||||
lastIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if lastIndex == -1 {
|
||||
return []string{key} // Fallback if parsing fails
|
||||
}
|
||||
|
||||
ip := key[:lastIndex]
|
||||
proxyConfigID := key[lastIndex+1:]
|
||||
|
||||
return []string{ip, proxyConfigID}
|
||||
}
|
||||
Reference in New Issue
Block a user