120 lines
4.7 KiB
Go
120 lines
4.7 KiB
Go
package routers
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/go-chi/jwtauth/v5"
|
|
api "github.com/tech/sendico/pkg/api/http"
|
|
"github.com/tech/sendico/pkg/api/http/response"
|
|
"github.com/tech/sendico/pkg/merrors"
|
|
"github.com/tech/sendico/pkg/model"
|
|
"github.com/tech/sendico/pkg/mservice"
|
|
"github.com/tech/sendico/pkg/mutil/mzap"
|
|
"github.com/tech/sendico/server/interface/api/sresponse"
|
|
emodel "github.com/tech/sendico/server/interface/model"
|
|
"github.com/tech/sendico/server/internal/api/routers/ipguard"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
type tokenHandlerFunc = func(r *http.Request, t *emodel.AccountToken) http.HandlerFunc
|
|
|
|
func (ar *AuthorizedRouter) validateClientPolicy(r *http.Request, t *emodel.AccountToken) http.HandlerFunc {
|
|
clientID := strings.TrimSpace(t.ClientID)
|
|
if clientID == "" {
|
|
// Legacy tokens without client_id remain valid until expiration.
|
|
return nil
|
|
}
|
|
client, err := ar.rtdb.GetClient(r.Context(), clientID)
|
|
if errors.Is(err, merrors.ErrNoData) || client == nil {
|
|
ar.logger.Debug("Client not found for access token", zap.String("client_id", clientID))
|
|
return response.Unauthorized(ar.logger, ar.service, "client not found")
|
|
}
|
|
if err != nil {
|
|
ar.logger.Warn("Failed to resolve client for access token", zap.Error(err), zap.String("client_id", clientID))
|
|
return response.Internal(ar.logger, ar.service, err)
|
|
}
|
|
if client.IsRevoked {
|
|
return response.Unauthorized(ar.logger, ar.service, "client has been revoked")
|
|
}
|
|
if client.AccountRef != nil && *client.AccountRef != t.AccountRef {
|
|
return response.Unauthorized(ar.logger, ar.service, "client account mismatch")
|
|
}
|
|
|
|
clientIP := ipguard.ClientIP(r)
|
|
allowed, err := ipguard.Allowed(clientIP, client.AllowedCIDRs)
|
|
if err != nil {
|
|
ar.logger.Warn("Client IP policy contains invalid CIDR", zap.Error(err), zap.String("client_id", clientID))
|
|
return response.Forbidden(ar.logger, ar.service, "client_ip_policy_invalid", "client ip policy is invalid")
|
|
}
|
|
if !allowed {
|
|
rawIP := ""
|
|
if clientIP != nil {
|
|
rawIP = clientIP.String()
|
|
}
|
|
ar.logger.Warn("Client IP policy denied authorized request", zap.String("client_id", clientID), zap.String("remote_ip", rawIP))
|
|
return response.Forbidden(ar.logger, ar.service, "ip_not_allowed", "request ip is not allowed for this client")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (ar *AuthorizedRouter) tokenHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler tokenHandlerFunc) {
|
|
hndlr := func(r *http.Request) http.HandlerFunc {
|
|
_, claims, err := jwtauth.FromContext(r.Context())
|
|
if err != nil {
|
|
ar.logger.Debug("Authorization failed", zap.Error(err), zap.String("request", r.URL.Path))
|
|
return response.Unauthorized(ar.logger, ar.service, "credentials required")
|
|
}
|
|
t, err := emodel.Claims2Token(claims)
|
|
if err != nil {
|
|
ar.logger.Debug("Failed to decode account token", zap.Error(err))
|
|
return response.BadRequest(ar.logger, ar.service, "credentials_unreadable", "faild to parse credentials")
|
|
}
|
|
if h := ar.validateClientPolicy(r, t); h != nil {
|
|
return h
|
|
}
|
|
return handler(r, t)
|
|
}
|
|
ar.imp.InstallHandler(service, endpoint, method, hndlr)
|
|
}
|
|
|
|
func (ar *AuthorizedRouter) AccountHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.AccountHandlerFunc) {
|
|
hndlr := func(r *http.Request, t *emodel.AccountToken) http.HandlerFunc {
|
|
if t.Pending {
|
|
return response.Unauthorized(ar.logger, ar.service, "additional verification required")
|
|
}
|
|
var a model.Account
|
|
if err := ar.db.Get(r.Context(), t.AccountRef, &a); err != nil {
|
|
if errors.Is(err, merrors.ErrNoData) {
|
|
ar.logger.Debug("Failed to find related user", zap.Error(err), mzap.AccRef(t.AccountRef))
|
|
return response.NotFound(ar.logger, ar.service, err.Error())
|
|
}
|
|
return response.Internal(ar.logger, ar.service, err)
|
|
}
|
|
accessToken, err := ar.imp.CreateAccessTokenForClient(&a, t.ClientID)
|
|
if err != nil {
|
|
ar.logger.Warn("Failed to generate access token", zap.Error(err))
|
|
return response.Internal(ar.logger, ar.service, err)
|
|
}
|
|
return handler(r, &a, &accessToken)
|
|
}
|
|
ar.tokenHandler(service, endpoint, method, hndlr)
|
|
}
|
|
|
|
func (ar *AuthorizedRouter) PendingAccountHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.PendingAccountHandlerFunc) {
|
|
hndlr := func(r *http.Request, t *emodel.AccountToken) http.HandlerFunc {
|
|
var a model.Account
|
|
if err := ar.db.Get(r.Context(), t.AccountRef, &a); err != nil {
|
|
if errors.Is(err, merrors.ErrNoData) {
|
|
ar.logger.Debug("Failed to find related user", zap.Error(err), mzap.AccRef(t.AccountRef))
|
|
return response.NotFound(ar.logger, ar.service, err.Error())
|
|
}
|
|
return response.Internal(ar.logger, ar.service, err)
|
|
}
|
|
return handler(r, &a, t)
|
|
}
|
|
ar.tokenHandler(service, endpoint, method, hndlr)
|
|
}
|