mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-09-10 07:01:01 +00:00
FB-1378: Use IP whitelisting for ingest authentication and authorization (#2070)
* Use IP whitelisting for ingest For ingest, use configured IPs to authenticate the requests. Auth-token will no longer be used for requests from ingest consumers. If IP in request is in configured IPs, authenticate and authorize as an admin. If IP in request is not in configured IPs, proceed with the standard authentication/authorization using ADD. * need to remove port from client IP * addressed review comments
This commit is contained in:
parent
62971f55dd
commit
3986e202bf
17 changed files with 844 additions and 90 deletions
|
|
@ -8,6 +8,7 @@ import (
|
|||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
|
@ -61,22 +62,23 @@ type Groups struct {
|
|||
|
||||
// Auth holds state, configuration, and utilities needed for authentication.
|
||||
type Auth struct {
|
||||
logger logger.Logger
|
||||
cookieName string
|
||||
secretKey []byte
|
||||
groupEndpoint string
|
||||
logoutEndpoint string
|
||||
fbURL string // fbURL is the domain featurebase is hosted on, used for post logout redirection
|
||||
oAuthConfig *oauth2.Config
|
||||
cacheTTL time.Duration // cacheTTL is used to determine if a cached item should be refreshed or not
|
||||
tokenTTR time.Duration // tokenTTR (time to refresh) is used to determine if a token should be refreshed or not
|
||||
tokenCache map[string]cachedToken // tokenCache is a map of accessToken -> *oauth2.Token which we can use to refresh the tokens
|
||||
groupsCache map[string]cachedGroups // groupsCache is a map of accessToken -> group memberships
|
||||
lastCacheClean time.Time // last cache clean is the time that the cache was last cleaned
|
||||
logger logger.Logger
|
||||
cookieName string
|
||||
secretKey []byte
|
||||
groupEndpoint string
|
||||
logoutEndpoint string
|
||||
fbURL string // fbURL is the domain featurebase is hosted on, used for post logout redirection
|
||||
oAuthConfig *oauth2.Config
|
||||
cacheTTL time.Duration // cacheTTL is used to determine if a cached item should be refreshed or not
|
||||
tokenTTR time.Duration // tokenTTR (time to refresh) is used to determine if a token should be refreshed or not
|
||||
tokenCache map[string]cachedToken // tokenCache is a map of accessToken -> *oauth2.Token which we can use to refresh the tokens
|
||||
groupsCache map[string]cachedGroups // groupsCache is a map of accessToken -> group memberships
|
||||
lastCacheClean time.Time // last cache clean is the time that the cache was last cleaned
|
||||
allowedNetworks []net.IPNet // list of allowed networks for ingest
|
||||
}
|
||||
|
||||
// NewAuth instantiates and returns a new Auth struct
|
||||
func NewAuth(logger logger.Logger, url string, scopes []string, authURL, tokenURL, groupEndpoint, logout, clientID, clientSecret, secretKey string) (auth *Auth, err error) {
|
||||
func NewAuth(logger logger.Logger, url string, scopes []string, authURL, tokenURL, groupEndpoint, logout, clientID, clientSecret, secretKey string, configuredIPs []string) (auth *Auth, err error) {
|
||||
auth = &Auth{
|
||||
logger: logger,
|
||||
cookieName: CookieName,
|
||||
|
|
@ -103,6 +105,13 @@ func NewAuth(logger logger.Logger, url string, scopes []string, authURL, tokenUR
|
|||
if auth.secretKey, err = decodeHex(secretKey); err != nil {
|
||||
return nil, errors.Wrap(err, "decoding secret key")
|
||||
}
|
||||
|
||||
// convert IPs and add them to allowed networks
|
||||
err = auth.convertIP(configuredIPs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return auth, nil
|
||||
}
|
||||
|
||||
|
|
@ -336,3 +345,39 @@ func decodeHex(hexstr string) ([]byte, error) {
|
|||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (a *Auth) convertIP(configuredIPs []string) error {
|
||||
sz := len(configuredIPs)
|
||||
nets := make([]net.IPNet, sz)
|
||||
for i, ip := range configuredIPs {
|
||||
// skip empty strings
|
||||
if ip == "" {
|
||||
sz--
|
||||
continue
|
||||
}
|
||||
// for IPs passed without a subnet, append /32 to only allow 1 IP
|
||||
// this step is needed because ParseCIDR method assumes a CIDR address
|
||||
if !strings.Contains(ip, "/") {
|
||||
ip = ip + "/32"
|
||||
}
|
||||
_, subnet, err := net.ParseCIDR(ip)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "parsing CIDR for %v", ip)
|
||||
}
|
||||
nets[i] = *subnet
|
||||
}
|
||||
a.allowedNetworks = nets[:sz]
|
||||
return nil
|
||||
}
|
||||
|
||||
// if IP is in allowed networks, then return true to grant admin permissions
|
||||
func (a *Auth) CheckAllowedNetworks(clientIP string) bool {
|
||||
clientIP = strings.Split(clientIP, ":")[0]
|
||||
convertedIP := net.ParseIP(clientIP)
|
||||
for _, network := range a.allowedNetworks {
|
||||
if network.Contains(convertedIP) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
|
|
@ -33,6 +34,7 @@ func NewTestAuth(t *testing.T) *Auth {
|
|||
LogoutURL = "https://login.microsoftonline.com/common/oauth2/v2.0/logout"
|
||||
Scopes = []string{"https://graph.microsoft.com/.default", "offline_access"}
|
||||
Key = "DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF"
|
||||
configuredIPs = []string{}
|
||||
)
|
||||
|
||||
a, err := NewAuth(
|
||||
|
|
@ -46,6 +48,7 @@ func NewTestAuth(t *testing.T) *Auth {
|
|||
ClientID,
|
||||
ClientSecret,
|
||||
Key,
|
||||
configuredIPs,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("building auth object%s", err)
|
||||
|
|
@ -147,6 +150,7 @@ func TestAuth(t *testing.T) {
|
|||
"e9088663-eb08-41d7-8f65-efb5f54bbb71",
|
||||
"DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF",
|
||||
"DEADBEEFD",
|
||||
[]string{},
|
||||
)
|
||||
if err == nil || !strings.Contains(err.Error(), "decoding secret key") {
|
||||
t.Fatalf("expected error decoding secret key got: %v", err)
|
||||
|
|
@ -648,3 +652,101 @@ func (s *ServerTransportStream) SetTrailer(md metadata.MD) error {
|
|||
_ = md
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestCheckAllowedNetworks(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
requestIP string
|
||||
configuredIPs []string
|
||||
isAdmin bool
|
||||
}{
|
||||
{
|
||||
requestIP: "10.0.0.1",
|
||||
configuredIPs: []string{"10.0.0.1"},
|
||||
isAdmin: true,
|
||||
},
|
||||
{
|
||||
requestIP: "10.0.0.3",
|
||||
configuredIPs: []string{"10.0.0.1", "10.0.0.2"},
|
||||
isAdmin: false,
|
||||
},
|
||||
{
|
||||
requestIP: "10.0.0.2",
|
||||
configuredIPs: []string{"10.0.0.1/30"},
|
||||
isAdmin: true,
|
||||
},
|
||||
// it is possible for the client IP to have a port
|
||||
{
|
||||
requestIP: "10.0.0.2:22",
|
||||
configuredIPs: []string{"10.0.0.1/30"},
|
||||
isAdmin: true,
|
||||
},
|
||||
{
|
||||
requestIP: "10.1.0.3",
|
||||
configuredIPs: []string{"10.0.0.1/32"},
|
||||
isAdmin: false,
|
||||
},
|
||||
{
|
||||
requestIP: "10.0.0.254",
|
||||
configuredIPs: []string{"10.0.0.1/24"},
|
||||
isAdmin: true,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("network-%d", i), func(t *testing.T) {
|
||||
a := NewTestAuth(t)
|
||||
if err := a.convertIP(test.configuredIPs); err != nil {
|
||||
t.Fatalf("failed to convert IPs from strings to net.IP: %v", err)
|
||||
}
|
||||
got := a.CheckAllowedNetworks(test.requestIP)
|
||||
if got != test.isAdmin {
|
||||
t.Fatalf("expected %v, got %v", test.isAdmin, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertIP(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
configuredIPs []string
|
||||
convertedIPs []net.IPNet
|
||||
}{
|
||||
{
|
||||
configuredIPs: []string{"10.0.0.1"},
|
||||
convertedIPs: []net.IPNet{
|
||||
{IP: net.ParseIP("10.0.0.1"), Mask: net.CIDRMask(32, 32)},
|
||||
},
|
||||
},
|
||||
{
|
||||
configuredIPs: []string{"10.0.0.1/30"},
|
||||
convertedIPs: []net.IPNet{
|
||||
{IP: net.ParseIP("10.0.0.0"), Mask: net.CIDRMask(30, 32)},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("network-%d", i), func(t *testing.T) {
|
||||
a := NewTestAuth(t)
|
||||
if err := a.convertIP(test.configuredIPs); err != nil {
|
||||
t.Fatalf("failed to convert IPs from strings to net.IP: %v", err)
|
||||
}
|
||||
|
||||
if len(a.allowedNetworks) != len(test.convertedIPs) {
|
||||
t.Fatalf("expected len of %v networks, got %v", len(test.convertedIPs), len(a.allowedNetworks))
|
||||
}
|
||||
|
||||
for i := range a.allowedNetworks {
|
||||
expected, got := test.convertedIPs[i], a.allowedNetworks[i]
|
||||
if got.IP.String() != expected.IP.String() {
|
||||
t.Fatalf("for IP, expected %v, got %v", expected.IP, got.IP)
|
||||
}
|
||||
if got.Mask.String() != expected.Mask.String() {
|
||||
t.Fatalf("for mask, expected %v, got %v", expected.Mask.String(), got.Mask.String())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -641,6 +641,7 @@ func TestImport_AuthOn(t *testing.T) {
|
|||
auth.ClientId,
|
||||
auth.ClientSecret,
|
||||
auth.SecretKey,
|
||||
[]string{},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
|
|
|||
|
|
@ -108,6 +108,7 @@ func BuildServerFlags(cmd *cobra.Command, srv *server.Command) {
|
|||
flags.StringVar(&srv.Config.Auth.SecretKey, "auth.secret-key", srv.Config.Auth.SecretKey, "Secret key used for auth.")
|
||||
flags.StringVar(&srv.Config.Auth.PermissionsFile, "auth.permissions", srv.Config.Auth.PermissionsFile, "Permissions' file with group authorization.")
|
||||
flags.StringVar(&srv.Config.Auth.QueryLogPath, "auth.query-log-path", srv.Config.Auth.QueryLogPath, "Path to log user queries")
|
||||
flags.StringSliceVar(&srv.Config.Auth.ConfiguredIPs, "auth.configured-ips", srv.Config.Auth.ConfiguredIPs, "List of configured IPs allowed for ingest")
|
||||
|
||||
flags.BoolVar(&srv.Config.DataDog.Enable, "datadog.enable", false, "enable continuous profiling with DataDog cloud service, Note you must have DataDog agent installed")
|
||||
flags.StringVar(&srv.Config.DataDog.Service, "datadog.service", "default-service", "The Datadog service name, for example my-web-app")
|
||||
|
|
|
|||
151
http_handler.go
151
http_handler.go
|
|
@ -93,6 +93,23 @@ var externalPrefixFlag = map[string]bool{
|
|||
"version": true,
|
||||
}
|
||||
|
||||
const (
|
||||
// OriginalIPHeader is the original IP for client
|
||||
// It is used mainly for authenticating on remote nodes
|
||||
// ForwardedIPHeader gets updated to the node's IP
|
||||
// when requests are forward to other nodes in the cluster
|
||||
OriginalIPHeader = "X-Molecula-Original-IP"
|
||||
|
||||
// ForwardedIPHeader is part of the standard header
|
||||
// it is used to identify the originating IP of a client
|
||||
ForwardedIPHeader = "X-Forwarded-For"
|
||||
|
||||
// AllowedNetworksGroupName is used for the admin group authorization
|
||||
// when authentication is completed through checking the client IP
|
||||
// against the allowed networks
|
||||
AllowedNetworksGroupName = "allowed-networks"
|
||||
)
|
||||
|
||||
type errorResponse struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
|
@ -581,35 +598,69 @@ func (h *Handler) chkInternal(handler http.HandlerFunc) http.HandlerFunc {
|
|||
}
|
||||
}
|
||||
|
||||
func (h *Handler) chkAllowedNetworks(r *http.Request) (bool, context.Context) {
|
||||
// for every request, get IP of the request and check against configured IPs
|
||||
reqIP := GetIP(r)
|
||||
if reqIP == "" {
|
||||
return false, r.Context()
|
||||
}
|
||||
|
||||
// if client IP is in allowed networks
|
||||
// add it to the context for key X-Molecula-Original-IP
|
||||
if h.auth.CheckAllowedNetworks(reqIP) {
|
||||
ctx := context.WithValue(r.Context(), OriginalIPHeader, reqIP)
|
||||
return true, ctx
|
||||
}
|
||||
return false, r.Context()
|
||||
}
|
||||
|
||||
func (h *Handler) chkAuthN(handler http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
if h.auth != nil {
|
||||
uinfo, err := h.auth.Authenticate(r.Context(), getToken(r))
|
||||
// if IP is in allowed networks, then serve the request
|
||||
allowedNetwork, ctx := h.chkAllowedNetworks(r)
|
||||
if allowedNetwork {
|
||||
handler.ServeHTTP(w, r.WithContext(ctx))
|
||||
return
|
||||
}
|
||||
|
||||
uinfo, err := h.auth.Authenticate(ctx, getToken(r))
|
||||
if err != nil {
|
||||
http.Error(w, errors.Wrap(err, "authenticating").Error(), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
// just in case it got refreshed
|
||||
h.auth.SetCookie(w, uinfo.Token, uinfo.Expiry)
|
||||
ctx = context.WithValue(ctx, "token", r.Header["Authorization"])
|
||||
}
|
||||
ctx := context.WithValue(r.Context(), "token", r.Header["Authorization"])
|
||||
handler.ServeHTTP(w, r.WithContext(ctx))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) chkAuthZ(handler http.HandlerFunc, perm authz.Permission) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// if auth isn't turned on, just serve the request
|
||||
if h.auth == nil {
|
||||
handler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// check if IP is in allowed networks, if yes give it admin permissions
|
||||
allowedNetwork, ctx := h.chkAllowedNetworks(r)
|
||||
if allowedNetwork {
|
||||
ctx = context.WithValue(ctx, contextKeyGroupMembership, []string{AllowedNetworksGroupName, h.permissions.Admin})
|
||||
handler.ServeHTTP(w, r.WithContext(ctx))
|
||||
return
|
||||
}
|
||||
|
||||
// make a copy of the requested permissions
|
||||
lperm := perm
|
||||
|
||||
// check if the user is authenticated
|
||||
uinfo, err := h.auth.Authenticate(r.Context(), getToken(r))
|
||||
uinfo, err := h.auth.Authenticate(ctx, getToken(r))
|
||||
if err != nil {
|
||||
http.Error(w, errors.Wrap(err, "authenticating").Error(), http.StatusForbidden)
|
||||
return
|
||||
|
|
@ -618,7 +669,7 @@ func (h *Handler) chkAuthZ(handler http.HandlerFunc, perm authz.Permission) http
|
|||
h.auth.SetCookie(w, uinfo.Token, uinfo.Expiry)
|
||||
|
||||
// put the user's groups in the context
|
||||
ctx := context.WithValue(r.Context(), contextKeyGroupMembership, uinfo.Groups)
|
||||
ctx = context.WithValue(ctx, contextKeyGroupMembership, uinfo.Groups)
|
||||
ctx = context.WithValue(ctx, "token", "Bearer "+uinfo.Token)
|
||||
|
||||
// unlikely h.permissions will be nil, but we'll check to be safe
|
||||
|
|
@ -630,7 +681,7 @@ func (h *Handler) chkAuthZ(handler http.HandlerFunc, perm authz.Permission) http
|
|||
|
||||
// figure out what the user is querying for
|
||||
queryString := ""
|
||||
queryRequest := r.Context().Value(contextKeyQueryRequest)
|
||||
queryRequest := ctx.Value(contextKeyQueryRequest)
|
||||
if req, ok := queryRequest.(*QueryRequest); ok {
|
||||
queryString = req.Query
|
||||
|
||||
|
|
@ -701,10 +752,27 @@ func (h *Handler) chkAuthZ(handler http.HandlerFunc, perm authz.Permission) http
|
|||
}
|
||||
|
||||
func GetIP(r *http.Request) string {
|
||||
forwarded := r.Header.Get("X-FORWARDED-FOR")
|
||||
if forwarded != "" {
|
||||
return forwarded
|
||||
// check if original IP was set in the request
|
||||
og := r.Header.Get(OriginalIPHeader)
|
||||
if og != "" {
|
||||
return og
|
||||
}
|
||||
|
||||
// check if original IP is in the context
|
||||
if ogIP, ok := r.Context().Value(OriginalIPHeader).(string); ok && ogIP != "" {
|
||||
return ogIP
|
||||
}
|
||||
|
||||
// X-Forwarded-For can have multiple IPs
|
||||
// the first IP will always be the originating client IP
|
||||
// the remaining IPs will be for any proxies the request went through
|
||||
forwarded := r.Header.Get(ForwardedIPHeader)
|
||||
forwardedList := strings.Split(forwarded, ",")
|
||||
if forwardedList[0] != "" {
|
||||
return forwardedList[0]
|
||||
|
||||
}
|
||||
|
||||
return r.RemoteAddr
|
||||
}
|
||||
|
||||
|
|
@ -888,6 +956,40 @@ func headerAcceptRoaringRow(header http.Header) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func (h *Handler) filterSchema(schema []*IndexInfo, g []authn.Group) []*IndexInfo {
|
||||
if !h.permissions.IsAdmin(g) {
|
||||
var filtered []*IndexInfo
|
||||
allowed := h.permissions.GetAuthorizedIndexList(g, authz.Read)
|
||||
for _, s := range schema {
|
||||
for _, index := range allowed {
|
||||
if s.Name == index {
|
||||
filtered = append(filtered, s)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
schema = filtered
|
||||
}
|
||||
return schema
|
||||
}
|
||||
|
||||
func (h *Handler) getGroupMembership(r *http.Request) (g []authn.Group) {
|
||||
// if IP is in allowed networks, then give admin group membership
|
||||
if h.auth.CheckAllowedNetworks(GetIP(r)) {
|
||||
g = []authn.Group{
|
||||
{
|
||||
GroupID: h.permissions.Admin,
|
||||
GroupName: AllowedNetworksGroupName,
|
||||
},
|
||||
}
|
||||
return g
|
||||
}
|
||||
|
||||
// check if group membership was already set in request when auth token was obtained
|
||||
g = r.Context().Value(contextKeyGroupMembership).([]authn.Group)
|
||||
return g
|
||||
}
|
||||
|
||||
// handleGetSchema handles GET /schema requests.
|
||||
func (h *Handler) handleGetSchema(w http.ResponseWriter, r *http.Request) {
|
||||
if !validHeaderAcceptJSON(r.Header) {
|
||||
|
|
@ -906,24 +1008,12 @@ func (h *Handler) handleGetSchema(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
// if auth is turned on, filter response to only include authorized indexes
|
||||
if h.auth != nil {
|
||||
g := r.Context().Value(contextKeyGroupMembership)
|
||||
g := h.getGroupMembership(r)
|
||||
if g == nil {
|
||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if !h.permissions.IsAdmin(g.([]authn.Group)) {
|
||||
var filtered []*IndexInfo
|
||||
allowed := h.permissions.GetAuthorizedIndexList(g.([]authn.Group), authz.Read)
|
||||
for _, s := range schema {
|
||||
for _, index := range allowed {
|
||||
if s.Name == index {
|
||||
filtered = append(filtered, s)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
schema = filtered
|
||||
}
|
||||
schema = h.filterSchema(schema, g)
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(Schema{Indexes: schema}); err != nil {
|
||||
|
|
@ -952,25 +1042,14 @@ func (h *Handler) handleGetSchemaDetails(w http.ResponseWriter, r *http.Request)
|
|||
|
||||
// if auth is turned on, filter response to only include authorized indexes
|
||||
if h.auth != nil {
|
||||
g := r.Context().Value(contextKeyGroupMembership)
|
||||
g := h.getGroupMembership(r)
|
||||
if g == nil {
|
||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if !h.permissions.IsAdmin(g.([]authn.Group)) {
|
||||
var filtered []*IndexInfo
|
||||
allowed := h.permissions.GetAuthorizedIndexList(g.([]authn.Group), authz.Read)
|
||||
for _, s := range schema {
|
||||
for _, index := range allowed {
|
||||
if s.Name == index {
|
||||
filtered = append(filtered, s)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
schema = filtered
|
||||
}
|
||||
schema = h.filterSchema(schema, g)
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(Schema{Indexes: schema}); err != nil {
|
||||
h.logger.Printf("write schema response error: %s", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package pilosa
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
|
@ -188,6 +189,19 @@ func readResponse(w *httptest.ResponseRecorder) ([]byte, error) {
|
|||
return ioutil.ReadAll(res.Body)
|
||||
}
|
||||
|
||||
// common variables used for testing auth
|
||||
var (
|
||||
ClientID = "e9088663-eb08-41d7-8f65-efb5f54bbb71"
|
||||
ClientSecret = "DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF"
|
||||
AuthorizeURL = "https://login.microsoftonline.com/4a137d66-d161-4ae4-b1e6-07e9920874b8/oauth2/v2.0/authorize"
|
||||
TokenURL = "https://login.microsoftonline.com/4a137d66-d161-4ae4-b1e6-07e9920874b8/oauth2/v2.0/token"
|
||||
GroupEndpointURL = "https://graph.microsoft.com/v1.0/me/transitiveMemberOf/microsoft.graph.group?$count=true"
|
||||
LogoutURL = "https://login.microsoftonline.com/common/oauth2/v2.0/logout"
|
||||
Scopes = []string{"https://graph.microsoft.com/.default", "offline_access"}
|
||||
Key = "DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF"
|
||||
ConfiguredIPs = []string{}
|
||||
)
|
||||
|
||||
func TestAuthentication(t *testing.T) {
|
||||
type evaluate func(w *httptest.ResponseRecorder, data []byte)
|
||||
type endpoint func(w http.ResponseWriter, r *http.Request)
|
||||
|
|
@ -221,34 +235,10 @@ func TestAuthentication(t *testing.T) {
|
|||
}))
|
||||
defer groupSrv.Close()
|
||||
|
||||
var (
|
||||
ClientId = "e9088663-eb08-41d7-8f65-efb5f54bbb71"
|
||||
ClientSecret = "DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF"
|
||||
AuthorizeURL = "https://login.microsoftonline.com/4a137d66-d161-4ae4-b1e6-07e9920874b8/oauth2/v2.0/authorize"
|
||||
TokenURL = "https://login.microsoftonline.com/4a137d66-d161-4ae4-b1e6-07e9920874b8/oauth2/v2.0/token"
|
||||
GroupEndpointURL = groupSrv.URL
|
||||
LogoutURL = "https://login.microsoftonline.com/common/oauth2/v2.0/logout"
|
||||
Scopes = []string{"https://graph.microsoft.com/.default", "offline_access"}
|
||||
SecretKey = "DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF"
|
||||
)
|
||||
GroupEndpointURL = groupSrv.URL
|
||||
secretKey, _ := hex.DecodeString(Key)
|
||||
|
||||
secretKey, _ := hex.DecodeString("DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF")
|
||||
|
||||
a, err := authn.NewAuth(
|
||||
logger.NewStandardLogger(os.Stdout),
|
||||
"http://localhost:10101/",
|
||||
Scopes,
|
||||
AuthorizeURL,
|
||||
TokenURL,
|
||||
GroupEndpointURL,
|
||||
LogoutURL,
|
||||
ClientId,
|
||||
ClientSecret,
|
||||
SecretKey,
|
||||
)
|
||||
if err != nil {
|
||||
t.Errorf("building auth object%s", err)
|
||||
}
|
||||
a := NewTestAuth(t)
|
||||
|
||||
h := Handler{
|
||||
logger: logger.NewStandardLogger(os.Stdout),
|
||||
|
|
@ -781,17 +771,6 @@ func TestChkInternal(t *testing.T) {
|
|||
|
||||
func NewTestAuth(t *testing.T) *authn.Auth {
|
||||
t.Helper()
|
||||
var (
|
||||
ClientID = "e9088663-eb08-41d7-8f65-efb5f54bbb71"
|
||||
ClientSecret = "DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF"
|
||||
AuthorizeURL = "https://login.microsoftonline.com/4a137d66-d161-4ae4-b1e6-07e9920874b8/oauth2/v2.0/authorize"
|
||||
TokenURL = "https://login.microsoftonline.com/4a137d66-d161-4ae4-b1e6-07e9920874b8/oauth2/v2.0/token"
|
||||
GroupEndpointURL = "https://graph.microsoft.com/v1.0/me/transitiveMemberOf/microsoft.graph.group?$count=true"
|
||||
LogoutURL = "https://login.microsoftonline.com/common/oauth2/v2.0/logout"
|
||||
Scopes = []string{"https://graph.microsoft.com/.default", "offline_access"}
|
||||
Key = "DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF"
|
||||
)
|
||||
|
||||
a, err := authn.NewAuth(
|
||||
logger.NewStandardLogger(os.Stdout),
|
||||
"http://localhost:10101/",
|
||||
|
|
@ -803,6 +782,7 @@ func NewTestAuth(t *testing.T) *authn.Auth {
|
|||
ClientID,
|
||||
ClientSecret,
|
||||
Key,
|
||||
ConfiguredIPs,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("building auth object%s", err)
|
||||
|
|
@ -846,3 +826,172 @@ func TestHandleGetDiskUsage(t *testing.T) {
|
|||
t.Fatalf("expected %v, got %v", http.StatusOK, resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthzAllowedIPs(t *testing.T) {
|
||||
tests := []struct {
|
||||
configuredIPs []string
|
||||
clientIP string
|
||||
statusCode int
|
||||
permission authz.Permission
|
||||
}{
|
||||
// client IP is in configured IP list
|
||||
{
|
||||
configuredIPs: []string{"10.0.0.0", "10.0.0.1", "10.0.2.0/32"},
|
||||
clientIP: "10.0.0.0",
|
||||
statusCode: http.StatusOK,
|
||||
permission: authz.Admin,
|
||||
},
|
||||
// client IP is in configured IP list, testing with CIDR address
|
||||
{
|
||||
configuredIPs: []string{"10.0.0.0/30"},
|
||||
clientIP: "10.0.0.1",
|
||||
statusCode: http.StatusOK,
|
||||
permission: authz.Write,
|
||||
},
|
||||
// client IP has multiple IPs in X-Forwarded-For header
|
||||
// originating IP is in configured IP list
|
||||
{
|
||||
configuredIPs: []string{"10.0.0.0", "10.0.0.1/32", "10.0.0.2"},
|
||||
clientIP: "10.0.0.2,10.0.0.255",
|
||||
statusCode: http.StatusOK,
|
||||
permission: authz.Read,
|
||||
},
|
||||
// client IP has multiple IPs in X-Forwarded-For header
|
||||
// originating IP is not in configured IP list
|
||||
{
|
||||
configuredIPs: []string{"10.0.0.0/30"},
|
||||
clientIP: "10.0.0.255,10.0.0.2",
|
||||
statusCode: http.StatusForbidden,
|
||||
permission: authz.Read,
|
||||
},
|
||||
// client IP is not in configured IP list
|
||||
{
|
||||
configuredIPs: []string{"10.0.0.0", "10.0.0.1", "10.0.2.0/32"},
|
||||
clientIP: "10.0.0.3",
|
||||
statusCode: http.StatusForbidden,
|
||||
permission: authz.Write,
|
||||
},
|
||||
// client IP is not in configured IP list
|
||||
// X-Forwarded-For header is an empty string
|
||||
{
|
||||
configuredIPs: []string{"10.0.0.0", "10.0.0.1", "10.0.2.0/32"},
|
||||
clientIP: "",
|
||||
statusCode: http.StatusForbidden,
|
||||
permission: authz.Write,
|
||||
},
|
||||
}
|
||||
|
||||
testingHandler := func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("good"))
|
||||
}
|
||||
|
||||
permissions1 := `"user-groups":
|
||||
"dca35310-ecda-4f23-86cd-876aee559900":
|
||||
"test": "write"
|
||||
admin: "ac97c9e2-346b-42a2-b6da-18bcb61a32fe"`
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("ChkAuthz-%d", i), func(t *testing.T) {
|
||||
ConfiguredIPs = test.configuredIPs
|
||||
a := NewTestAuth(t)
|
||||
|
||||
h := Handler{
|
||||
logger: logger.NewStandardLogger(os.Stdout),
|
||||
queryLogger: logger.NewStandardLogger(os.Stdout),
|
||||
auth: a,
|
||||
}
|
||||
|
||||
var p authz.GroupPermissions
|
||||
if err := p.ReadPermissionsFile(strings.NewReader(permissions1)); err != nil {
|
||||
t.Errorf("Error: %s", err)
|
||||
}
|
||||
h.permissions = &p
|
||||
|
||||
r := httptest.NewRequest("GET", "/index/authz-abcd", nil)
|
||||
r.Header.Set(ForwardedIPHeader, test.clientIP)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler := h.chkAuthZ(testingHandler, authz.Read)
|
||||
handler(w, r)
|
||||
resp := w.Result()
|
||||
if resp.StatusCode != test.statusCode {
|
||||
t.Fatalf("expected %v, got %v", test.statusCode, resp.StatusCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthnAllowedIPs(t *testing.T) {
|
||||
IPList := []string{"10.0.0.0", "10.0.0.1", "10.0.0.2"}
|
||||
ValidForwardedIP := "10.0.0.0, 10.0.0.3, 10.0.0.4"
|
||||
InvalidForwardedIP := "10.0.0.3, 10.0.0.4"
|
||||
|
||||
tests := []struct {
|
||||
configuredIPs []string
|
||||
clientIP string
|
||||
statusCode int
|
||||
secretKey string
|
||||
}{
|
||||
// test client IP was in configured IP list - happy path
|
||||
{
|
||||
configuredIPs: IPList,
|
||||
clientIP: IPList[0],
|
||||
statusCode: http.StatusOK,
|
||||
},
|
||||
// test empty configured IP list
|
||||
{
|
||||
configuredIPs: []string{""},
|
||||
clientIP: IPList[0],
|
||||
statusCode: http.StatusUnauthorized,
|
||||
},
|
||||
// test client IP is not in configured IP list
|
||||
{
|
||||
configuredIPs: IPList,
|
||||
clientIP: "10.0.0.4",
|
||||
statusCode: http.StatusUnauthorized,
|
||||
},
|
||||
// test multiple client IPs in X-forwarded-IP
|
||||
// originating IP is in configured IP list
|
||||
{
|
||||
configuredIPs: IPList,
|
||||
clientIP: ValidForwardedIP,
|
||||
statusCode: http.StatusOK,
|
||||
},
|
||||
// test multiple client IPs in X-forwarded-IP
|
||||
// originating IP is not in configured IP list
|
||||
{
|
||||
configuredIPs: IPList,
|
||||
clientIP: InvalidForwardedIP,
|
||||
statusCode: http.StatusUnauthorized,
|
||||
},
|
||||
}
|
||||
|
||||
testingHandler := func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("good"))
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("ChkAuthn-%d", i), func(t *testing.T) {
|
||||
ConfiguredIPs = test.configuredIPs
|
||||
a := NewTestAuth(t)
|
||||
|
||||
h := Handler{
|
||||
logger: logger.NewStandardLogger(os.Stdout),
|
||||
queryLogger: logger.NewStandardLogger(os.Stdout),
|
||||
auth: a,
|
||||
}
|
||||
|
||||
r := httptest.NewRequest("GET", "/index/authn-abcd", nil)
|
||||
r.Header.Set(ForwardedIPHeader, test.clientIP)
|
||||
r = r.WithContext(context.Background())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler := h.chkAuthN(testingHandler)
|
||||
handler(w, r)
|
||||
resp := w.Result()
|
||||
if resp.StatusCode != test.statusCode {
|
||||
t.Fatalf("expected %v, got %v", test.statusCode, resp.StatusCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,12 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
gohttp "net/http"
|
||||
"os"
|
||||
"path"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
|
|
@ -469,3 +473,195 @@ func TestTranslationHandlers(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthAllowedNetworks(t *testing.T) {
|
||||
permissions1 := `
|
||||
"user-groups":
|
||||
"dca35310-ecda-4f23-86cd-876aee55906b":
|
||||
"test": "read"
|
||||
admin: "ac97c9e2-346b-42a2-b6da-18bcb61a32fe"`
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
permissionsPath := path.Join(tmpDir, "test-permissions.yaml")
|
||||
err := ioutil.WriteFile(permissionsPath, []byte(permissions1), 0666)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to write permissions file: %v", err)
|
||||
}
|
||||
|
||||
queryLogPath := path.Join(tmpDir, "query.log")
|
||||
_, err = os.Create(queryLogPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
validIP := "10.0.0.2"
|
||||
|
||||
clusterSize := 3
|
||||
commandOpts := make([][]server.CommandOption, clusterSize)
|
||||
configs := make([]*server.Config, clusterSize)
|
||||
for i := range configs {
|
||||
conf := server.NewConfig()
|
||||
configs[i] = conf
|
||||
conf.TLS.CertificatePath = "./testdata/certs/localhost.crt"
|
||||
conf.TLS.CertificateKeyPath = "./testdata/certs/localhost.key"
|
||||
conf.Auth.Enable = true
|
||||
conf.Auth.ClientId = "e9088663-eb08-41d7-8f65-efb5f54bbb71"
|
||||
conf.Auth.ClientSecret = "DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF"
|
||||
conf.Auth.AuthorizeURL = "https://login.microsoftonline.com/4a137d66-d161-4ae4-b1e6-07e9920874b8/oauth2/v2.0/authorize"
|
||||
conf.Auth.TokenURL = "https://login.microsoftonline.com/4a137d66-d161-4ae4-b1e6-07e9920874b8/oauth2/v2.0/authorize"
|
||||
conf.Auth.GroupEndpointURL = "https://graph.microsoft.com/v1.0/me/transitiveMemberOf/microsoft.graph.group?$count=true"
|
||||
conf.Auth.LogoutURL = "https://login.microsoftonline.com/common/oauth2/v2.0/logout"
|
||||
conf.Auth.RedirectBaseURL = "https://localhost:10101/"
|
||||
conf.Auth.QueryLogPath = queryLogPath
|
||||
conf.Auth.SecretKey = "DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF"
|
||||
conf.Auth.PermissionsFile = permissionsPath
|
||||
conf.Auth.Scopes = []string{"https://graph.microsoft.com/.default", "offline_access"}
|
||||
conf.Auth.ConfiguredIPs = []string{validIP}
|
||||
commandOpts[i] = append(commandOpts[i], server.OptCommandConfig(conf))
|
||||
}
|
||||
|
||||
c := test.MustRunCluster(t, clusterSize, commandOpts...)
|
||||
defer c.Close()
|
||||
|
||||
m := c.GetPrimary()
|
||||
index := "allowed-networks-index"
|
||||
keyedIndex := "allowed-networks-index-keyed"
|
||||
field := "field1"
|
||||
|
||||
// needed for key translation
|
||||
nameBytes, err := json.Marshal([]string{"a", "b", "c"})
|
||||
if err != nil {
|
||||
t.Fatalf("marshalling json: %v", err)
|
||||
}
|
||||
names := string(nameBytes)
|
||||
|
||||
schema := `
|
||||
{
|
||||
"index-name": "allowed-networks-index-keyed",
|
||||
"primary-key-type": "string",
|
||||
"index-action": "create",
|
||||
"fields": [
|
||||
{
|
||||
"field-name": "stringset",
|
||||
"field-type": "string",
|
||||
"field-options": {
|
||||
"cache-type": "ranked",
|
||||
"cache-size": 100000
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
|
||||
IPTests := []struct {
|
||||
TestName string
|
||||
ClientIP string
|
||||
StatusCode int
|
||||
}{
|
||||
{TestName: "ValidIP", ClientIP: validIP, StatusCode: http.StatusOK},
|
||||
{TestName: "InvalidIP", ClientIP: "10.0.1.1", StatusCode: http.StatusForbidden},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
testName string
|
||||
method string
|
||||
url string
|
||||
body string
|
||||
}{
|
||||
{
|
||||
testName: "Post-Index",
|
||||
method: "POST",
|
||||
url: fmt.Sprintf("%s/index/%s", m.URL(), index),
|
||||
body: "",
|
||||
},
|
||||
{
|
||||
testName: "Post-field",
|
||||
method: "POST",
|
||||
url: fmt.Sprintf("%s/index/%s/field/%s", m.URL(), index, field),
|
||||
body: "",
|
||||
},
|
||||
{
|
||||
testName: "Get-Schema",
|
||||
method: "GET",
|
||||
url: fmt.Sprintf("%s/schema", m.URL()),
|
||||
body: "",
|
||||
},
|
||||
{
|
||||
testName: "Post-Schema",
|
||||
method: "POST",
|
||||
url: fmt.Sprintf("%s/internal/schema", m.URL()),
|
||||
body: schema,
|
||||
},
|
||||
{
|
||||
testName: "Get-Shards",
|
||||
method: "GET",
|
||||
url: fmt.Sprintf("%s/internal/index/%s/shards", m.URL(), keyedIndex),
|
||||
body: "",
|
||||
},
|
||||
{
|
||||
testName: "Post-CreateKeys",
|
||||
method: "POST",
|
||||
url: fmt.Sprintf("%s/internal/translate/index/%s/keys/create", m.URL(), keyedIndex),
|
||||
body: names,
|
||||
},
|
||||
{
|
||||
testName: "Get-MemoryUsage",
|
||||
method: "GET",
|
||||
url: fmt.Sprintf("%s/internal/mem-usage", m.URL()),
|
||||
body: "",
|
||||
},
|
||||
{
|
||||
testName: "Get-Status",
|
||||
method: "GET",
|
||||
url: fmt.Sprintf("%s/status", m.URL()),
|
||||
body: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, ipTest := range IPTests {
|
||||
for _, test := range tests {
|
||||
t.Run(ipTest.TestName+"-"+test.testName, func(t *testing.T) {
|
||||
var req *gohttp.Request
|
||||
if test.body != "" {
|
||||
req, err = gohttp.NewRequest(test.method, test.url, strings.NewReader(test.body))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
} else {
|
||||
req, err = gohttp.NewRequest(test.method, test.url, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Forwarded-For", ipTest.ClientIP)
|
||||
resp, err := gohttp.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to send request: %v", err)
|
||||
}
|
||||
if resp.StatusCode != ipTest.StatusCode {
|
||||
t.Fatalf("expected %v response code, got %v, body: %v", ipTest.StatusCode, resp.StatusCode, resp.Body)
|
||||
}
|
||||
|
||||
if ipTest.StatusCode == 200 {
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("reading resp body :%v", err)
|
||||
}
|
||||
|
||||
if test.testName == "Post-CreateKeys" {
|
||||
var results map[string]uint64
|
||||
err = json.Unmarshal([]byte(body), &results)
|
||||
if err != nil {
|
||||
t.Fatalf("unmarshalling result: %v", err)
|
||||
}
|
||||
if len(results) != 3 {
|
||||
t.Fatalf("finding keys before any were set: expected 3 results, got %d (%q)", len(results), results)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ func GetAuthToken(t *testing.T) string {
|
|||
ClientID,
|
||||
ClientSecret,
|
||||
Key,
|
||||
[]string{},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("NewAuth: %v", err)
|
||||
|
|
|
|||
|
|
@ -154,6 +154,12 @@ func AddAuthToken(ctx context.Context, req *http.Request) *http.Request {
|
|||
// UserInfo.Token is not prefixed with "Bearer"
|
||||
req.Header.Set("Authorization", "Bearer "+uinfo.(*authn.UserInfo).Token)
|
||||
}
|
||||
|
||||
// set ogIP to request for remote calls
|
||||
if ogIP, ok := ctx.Value(OriginalIPHeader).(string); ok && ogIP != "" {
|
||||
req.Header.Set(OriginalIPHeader, ogIP)
|
||||
}
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1603,4 +1603,15 @@ func TestAddAuthToken(t *testing.T) {
|
|||
t.Fatalf("got '%v', expected '%v'", got, tok)
|
||||
}
|
||||
})
|
||||
t.Run("originalIP", func(t *testing.T) {
|
||||
req, err := gohttp.NewRequest("GET", "dontmatternone", strings.NewReader("this doesn't matter"))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
ogIP := "10.0.0.1"
|
||||
pilosa.AddAuthToken(context.WithValue(context.Background(), pilosa.OriginalIPHeader, ogIP), req)
|
||||
if got := req.Header.Get(pilosa.OriginalIPHeader); got != ogIP {
|
||||
t.Fatalf("got '%v', expected '%v'", got, ogIP)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,9 +16,8 @@ done
|
|||
|
||||
HOST=${HOSTS[2]}
|
||||
|
||||
ADMIN_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiYWRtaW4ifQ.I1iCgk1VU7m6e-En4ACTHIs6V2dZpy_8j2blSSo7K3U"
|
||||
# ingest string key data to user index
|
||||
/data/datagen --source custom --custom-config /data/tests/sup218_datagen.yaml --pilosa.index=user --pilosa.hosts=https://$HOST:10101 --pilosa.batch-size=1000 --auth-token=$ADMIN_TOKEN
|
||||
/data/datagen --source custom --custom-config /data/tests/sup218_datagen.yaml --pilosa.index=user --pilosa.hosts=https://$HOST:10101 --pilosa.batch-size=1000
|
||||
ifErr "running datagen on $HOST"
|
||||
|
||||
# install grpcurl
|
||||
|
|
|
|||
|
|
@ -128,6 +128,7 @@ long-query-time = "10s"
|
|||
secret-key = "98995f0530eeba96da1d0a04311073c0abb7b6abbfb0f5f4ef3629527ff88428"
|
||||
permissions = "/etc/permissions.yml"
|
||||
query-log-path = "/data/featurebase/query.log"
|
||||
configured-ips = ["${DEPLOYED_INGEST_IPS}"]
|
||||
EOT
|
||||
|
||||
echo "writing the permissions file"
|
||||
|
|
|
|||
|
|
@ -254,6 +254,7 @@ type Auth struct {
|
|||
SecretKey string `toml:"secret-key"`
|
||||
PermissionsFile string `toml:"permissions"`
|
||||
QueryLogPath string `toml:"query-log-path"`
|
||||
ConfiguredIPs []string `toml:"configured-ips"`
|
||||
}
|
||||
|
||||
// Namespace returns the namespace to use based on the Future flag.
|
||||
|
|
@ -663,6 +664,39 @@ func (c *Config) ValidateAuth() (errors []error) {
|
|||
errors = append(errors, fmt.Errorf("must provide scope for authentication with IdP - for access and refresh token"))
|
||||
}
|
||||
|
||||
if len(c.Auth.ConfiguredIPs) > 0 {
|
||||
for _, IP := range c.Auth.ConfiguredIPs {
|
||||
if strings.Contains(IP, ":") {
|
||||
errors = append(errors, fmt.Errorf("port is not allowed in IP %v for auth.configured-ips", IP))
|
||||
continue
|
||||
}
|
||||
if IP == "" {
|
||||
errors = append(errors, fmt.Errorf("empty string for auth.configured-ips"))
|
||||
continue
|
||||
}
|
||||
if strings.Contains(IP, "localhost") {
|
||||
errors = append(errors, fmt.Errorf("%v is not a valid IP for auth.configured-ips, DNS names are not allowed", IP))
|
||||
continue
|
||||
}
|
||||
if strings.EqualFold(IP, "0.0.0.0") {
|
||||
errors = append(errors, fmt.Errorf("%v is not a valid IP for auth.configured-ips", IP))
|
||||
continue
|
||||
}
|
||||
// validate CIDR addresses
|
||||
if strings.Contains(IP, "/") {
|
||||
if _, _, err := net.ParseCIDR(IP); err != nil {
|
||||
errors = append(errors, fmt.Errorf("%v is not a valid IP for auth.configured-ips: %v", IP, err))
|
||||
continue
|
||||
}
|
||||
continue
|
||||
}
|
||||
// validate IP
|
||||
if net.ParseIP(IP) == nil {
|
||||
errors = append(errors, fmt.Errorf("%v is not a valid IP for auth.configured-ips", IP))
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -281,6 +281,8 @@ func TestConfig_validateAuth(t *testing.T) {
|
|||
errorMesgURL := "invalid URL"
|
||||
errorMesgScope := "must provide scope"
|
||||
errorMesgKey := "invalid key length"
|
||||
errorMesgIP := "not a valid IP for auth.configured-ips"
|
||||
errorMesgPort := "port is not allowed in IP"
|
||||
validTestURL := "https://url.com/"
|
||||
validClientID := "clientid"
|
||||
validClientSecret := "clientSecret"
|
||||
|
|
@ -290,6 +292,10 @@ func TestConfig_validateAuth(t *testing.T) {
|
|||
validStringSlice := []string{"https://graph.microsoft.com/.default", "offline_access"}
|
||||
validString := "asdfqwer1234asdfzxcv"
|
||||
var emptySlice []string
|
||||
validIPList := []string{"10.0.0.1", "10.0.0.0", "10.0.0.0/32"}
|
||||
invalidIPList := []string{"0.0.0.0", "localhost", "100.0.1", "100.0.0/32"}
|
||||
emptyStringIP := []string{""}
|
||||
portIP := []string{"10.0.0.1:10101"}
|
||||
|
||||
enable := true
|
||||
disable := false
|
||||
|
|
@ -321,6 +327,7 @@ func TestConfig_validateAuth(t *testing.T) {
|
|||
LogoutURL: emptyString,
|
||||
Scopes: validStringSlice,
|
||||
SecretKey: emptyString,
|
||||
ConfiguredIPs: emptySlice,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -339,6 +346,7 @@ func TestConfig_validateAuth(t *testing.T) {
|
|||
LogoutURL: validTestURL,
|
||||
Scopes: validStringSlice,
|
||||
SecretKey: validString,
|
||||
ConfiguredIPs: validIPList,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -359,6 +367,7 @@ func TestConfig_validateAuth(t *testing.T) {
|
|||
LogoutURL: invalidURL,
|
||||
Scopes: validStringSlice,
|
||||
SecretKey: validKey,
|
||||
ConfiguredIPs: validIPList,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -377,6 +386,7 @@ func TestConfig_validateAuth(t *testing.T) {
|
|||
LogoutURL: validTestURL,
|
||||
Scopes: emptySlice,
|
||||
SecretKey: validKey,
|
||||
ConfiguredIPs: validIPList,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -394,6 +404,7 @@ func TestConfig_validateAuth(t *testing.T) {
|
|||
Scopes: validStringSlice,
|
||||
SecretKey: validKey,
|
||||
QueryLogPath: "thisIsAPAth",
|
||||
ConfiguredIPs: validIPList,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -410,6 +421,67 @@ func TestConfig_validateAuth(t *testing.T) {
|
|||
LogoutURL: validTestURL,
|
||||
Scopes: validStringSlice,
|
||||
SecretKey: emptyString,
|
||||
ConfiguredIPs: emptySlice,
|
||||
},
|
||||
},
|
||||
{
|
||||
// Auth enabled, all configs are set properly except configuredIPs
|
||||
[]string{
|
||||
errorMesgIP,
|
||||
errorMesgIP,
|
||||
errorMesgIP,
|
||||
errorMesgIP,
|
||||
},
|
||||
Auth{
|
||||
Enable: enable,
|
||||
ClientId: validClientID,
|
||||
ClientSecret: validClientSecret,
|
||||
AuthorizeURL: validTestURL,
|
||||
TokenURL: validTestURL,
|
||||
GroupEndpointURL: validTestURL,
|
||||
RedirectBaseURL: validTestURL,
|
||||
LogoutURL: validTestURL,
|
||||
Scopes: validStringSlice,
|
||||
SecretKey: validKey,
|
||||
ConfiguredIPs: invalidIPList,
|
||||
},
|
||||
},
|
||||
{
|
||||
// Auth enabled, all configs are set properly except configuredIPs
|
||||
[]string{
|
||||
errorMesgEmpty,
|
||||
},
|
||||
Auth{
|
||||
Enable: enable,
|
||||
ClientId: validClientID,
|
||||
ClientSecret: validClientSecret,
|
||||
AuthorizeURL: validTestURL,
|
||||
TokenURL: validTestURL,
|
||||
GroupEndpointURL: validTestURL,
|
||||
RedirectBaseURL: validTestURL,
|
||||
LogoutURL: validTestURL,
|
||||
Scopes: validStringSlice,
|
||||
SecretKey: validKey,
|
||||
ConfiguredIPs: emptyStringIP,
|
||||
},
|
||||
},
|
||||
{
|
||||
// Auth enabled, all configs are set properly except configuredIPs
|
||||
[]string{
|
||||
errorMesgPort,
|
||||
},
|
||||
Auth{
|
||||
Enable: enable,
|
||||
ClientId: validClientID,
|
||||
ClientSecret: validClientSecret,
|
||||
AuthorizeURL: validTestURL,
|
||||
TokenURL: validTestURL,
|
||||
GroupEndpointURL: validTestURL,
|
||||
RedirectBaseURL: validTestURL,
|
||||
LogoutURL: validTestURL,
|
||||
Scopes: validStringSlice,
|
||||
SecretKey: validKey,
|
||||
ConfiguredIPs: portIP,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -531,7 +531,7 @@ func (m *Command) SetupServer() error {
|
|||
}
|
||||
|
||||
ac := m.Config.Auth
|
||||
m.auth, err = authn.NewAuth(m.logger, ac.RedirectBaseURL, ac.Scopes, ac.AuthorizeURL, ac.TokenURL, ac.GroupEndpointURL, ac.LogoutURL, ac.ClientId, ac.ClientSecret, ac.SecretKey)
|
||||
m.auth, err = authn.NewAuth(m.logger, ac.RedirectBaseURL, ac.Scopes, ac.AuthorizeURL, ac.TokenURL, ac.GroupEndpointURL, ac.LogoutURL, ac.ClientId, ac.ClientSecret, ac.SecretKey, ac.ConfiguredIPs)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "instantiating authN object")
|
||||
}
|
||||
|
|
@ -544,6 +544,9 @@ func (m *Command) SetupServer() error {
|
|||
m.queryLogger.Infof("Starting Featurebase...")
|
||||
m.queryLogger.Infof("Group with admin level access: %v", p.Admin)
|
||||
m.queryLogger.Infof("Permissions: %+v", p.Permissions)
|
||||
if len(ac.ConfiguredIPs) > 0 {
|
||||
m.queryLogger.Infof("Configured IPs for allowed networks: %v", ac.ConfiguredIPs)
|
||||
}
|
||||
|
||||
// disable postgres binding if auth is enabled
|
||||
m.Config.Postgres.Bind = ""
|
||||
|
|
|
|||
26
testdata/certs/localhost.crt
vendored
Normal file
26
testdata/certs/localhost.crt
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIEcDCCAtigAwIBAgIQUcwmfB3r1kcWpRm+pJCNQTANBgkqhkiG9w0BAQsFADCB
|
||||
mTEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMTcwNQYDVQQLDC5zb3Vo
|
||||
YWlsYW5vb3JAc291aGFpbGFzLW1icC5sYW4gKFNvdWhhaWxhIE5vb3IpMT4wPAYD
|
||||
VQQDDDVta2NlcnQgc291aGFpbGFub29yQHNvdWhhaWxhcy1tYnAubGFuIChTb3Vo
|
||||
YWlsYSBOb29yKTAeFw0yMjAyMDMxNzAxNTRaFw0yNDA1MDMxNjAxNTRaMGIxJzAl
|
||||
BgNVBAoTHm1rY2VydCBkZXZlbG9wbWVudCBjZXJ0aWZpY2F0ZTE3MDUGA1UECwwu
|
||||
c291aGFpbGFub29yQHNvdWhhaWxhcy1tYnAubGFuIChTb3VoYWlsYSBOb29yKTCC
|
||||
ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKlW+aTutSSDraStNa3T55eH
|
||||
qR0F1Fxap7Zefd3TQuhNfZc7x9pyT8Rfd8n1fiL+NVAsrGWCX4kqrq5J3comPEZG
|
||||
zphoWBlhko9jWWa375xTEp4Bo8lQQ1Z0GgucVk8XkmPPO7z6ZuMb2+LBtY1IkXcP
|
||||
1nr0m/F1isQw2R5V51eBuNv44h0+CN+f706u5zsBrY4+wXS3l/hXzTEYAN/qryEi
|
||||
xjTsvlURnZHEKNtC1QPOGJMe/Viqv3HW9vLs9r5b9+9R7mt3nSWXLHJWe5e8P3xY
|
||||
s1aRWWWyd4F6q+0NYdlGN9SjN3MjaVuXCL76vvGaWmRAlYXm7y/+fIB8OIS/aV8C
|
||||
AwEAAaNqMGgwDgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMB8G
|
||||
A1UdIwQYMBaAFJzPC26j5NzKZj55kwlZB9ucEdk8MCAGA1UdEQQZMBeCCWxvY2Fs
|
||||
aG9zdIcEAAAAAIcEwKhWLzANBgkqhkiG9w0BAQsFAAOCAYEAdTR/I1ONyQe/KKYh
|
||||
R5pEWnAS7gGIW7EHkNoDhm/fv2+yplM7jpI7F6zBFIKIchSEWVSndnEerZ8lTcgG
|
||||
27fuvmR4B0EpLQ3CK5jWNMMDEgSpX5pRMn6ZOuhRDglt0/+knsxra3rbUORfrl+w
|
||||
FNAB7e1Q/q4eKVqxAiW0wpQEBnw3ko2/r2t1/x1diReDlb7NFztfwAxXyQKQbQvY
|
||||
/V6r514cm7s4qqnEudaK8LR8G5wrTBJgRdwzkpiBsi0Kekibp2FUQcRQeUvPqgvn
|
||||
c2VmJGVlWNJj5fhQO3Tn3NGLECA6jJFWVCMBHNP/DJtZjJQQhXyn4WC5r0g/aFwm
|
||||
iDX2djhj43GyMT7V1fTirJpWQC4zOMIV54nZ90BULJD76+hmLrBccdZPcJHhpPw/
|
||||
WGvhV+IEjxLvr/RPgcZitFwYKKEMgQcGRl6oLo71hD99cEQXhM5bgv7uvYvX4XqG
|
||||
W1S7Qst6xEkZ5nLMv0dLpd7SRCUXuIyNBUMkuAW7c2NY21lC
|
||||
-----END CERTIFICATE-----
|
||||
28
testdata/certs/localhost.key
vendored
Normal file
28
testdata/certs/localhost.key
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCpVvmk7rUkg62k
|
||||
rTWt0+eXh6kdBdRcWqe2Xn3d00LoTX2XO8fack/EX3fJ9X4i/jVQLKxlgl+JKq6u
|
||||
Sd3KJjxGRs6YaFgZYZKPY1lmt++cUxKeAaPJUENWdBoLnFZPF5Jjzzu8+mbjG9vi
|
||||
wbWNSJF3D9Z69JvxdYrEMNkeVedXgbjb+OIdPgjfn+9Oruc7Aa2OPsF0t5f4V80x
|
||||
GADf6q8hIsY07L5VEZ2RxCjbQtUDzhiTHv1Yqr9x1vby7Pa+W/fvUe5rd50llyxy
|
||||
VnuXvD98WLNWkVllsneBeqvtDWHZRjfUozdzI2lblwi++r7xmlpkQJWF5u8v/nyA
|
||||
fDiEv2lfAgMBAAECggEAWxRFp5kgYqhUT9s9oOX1jUHRCqQlCRtVUzhVeGb1eJ6d
|
||||
KydjIbIUBynV8xEQK+AQarPscZiCq/MCCwn9uYmBJ8dTvRN6NwSM4IRuBFpwDON9
|
||||
gvCc2F1MKoa9r3sBzP5/pSFEHyuLBSHpzXekn33lh2VEVdOUwHUZEta3IKLRj/V8
|
||||
eFNGLFGaRkTuVnjJpCySy89ANUxH7wXyz9g7S6WIvU0Gri6hh5JV5DZ0BYQWqrXs
|
||||
obipibRiTtSjBbV3uRABlA6le8ffWR9VsiS1njNRiXDvTl/zZKQjOCQxYJ4hbKHK
|
||||
BYZuz5mXxQSmZoLkFd3ttIQ98zGJGVR5HI20kt97UQKBgQDHPlB/38QUiLSRLWc0
|
||||
dDziXIAlk5X0IkYDdSj0mMi5OaSOVjtCCtzQO8ncuFDJv/bjhFCrNsaLhDrSMP9w
|
||||
q+X5C6IyFfZoxhFm0Yjf87szJ4Gj3LKwL33y62+4a+FoL8e2rKU2BNhC/hs+hLv+
|
||||
GIg0WPUD6ApQqEzZiEy9sPwrlwKBgQDZk/TtmSO2vSsV4sUSdFYiKU/0ngsFw1yh
|
||||
aAOQV5km3rqQP0bOmHvVmp8ZWWg0mRecEsih344kB1s3g60m6dfTYsfX+PCn4LoQ
|
||||
bB/b3VfJmq5mHeicydI9vpggHOo4HXRvDFjuXcDuq8WCflR+RfWPDRMtr+fnOtYz
|
||||
XzT/mv2JeQKBgDOUWDaicRp3wXcL7/nOVaysEaioqltHPCTNActAekYpPAZ1IGYa
|
||||
dcuajsmLFa8E+R8xM8j/JysbEjcz5A0BE0oDzvt0YBQDoqGhgPOpHz9A7PjEu6WM
|
||||
xehLNuLhWrskE2mhDCwN2QaOzfLDXf+Lzkbu+I8IVRNIXoy6ElPRK5cFAoGAUcy1
|
||||
dapwaeB+1VCXZNPGGjvL6pyMalNDfQ8838R/OMTeASM4/K0JSYpDLGWXrsarwKqz
|
||||
MWB58QOvAJUJwSqDUN6/YWwfFg6ABXKRG7kAqzPzQ0MVU/TwjPQd/1y/le9E+gb7
|
||||
XEp1IYYE0IWsQHkO1ARHZJLQrfdJ1rCBnWNo1IkCgYEAtqgGtOgYb169TjftqDZD
|
||||
7DdFUI6sweRfeRs91OGbLRpHU7LGia6GSm+CA8newUMJQtgT8EOR+OyDJBZTL1CS
|
||||
dkk0bmW3Z+C0zBSKhNlEKE5Jg6rDJ2fpC87lWN+6ZbNOZbjiMkV4gPlVFy8fkSCn
|
||||
5fSf7UT9/FFCrqKPFHPThXE=
|
||||
-----END PRIVATE KEY-----
|
||||
Loading…
Add table
Reference in a new issue