mirror of
https://github.com/pomerium/pomerium.git
synced 2025-05-17 02:57:11 +02:00
proxy: add route portal json (#5428)
* proxy: add route portal json * fix 405 issue * add link to issue * Update proxy/portal/filter_test.go Co-authored-by: Kenneth Jenkins <51246568+kenjenkins@users.noreply.github.com> --------- Co-authored-by: Kenneth Jenkins <51246568+kenjenkins@users.noreply.github.com>
This commit is contained in:
parent
6e1fabec0b
commit
e816cef2a1
10 changed files with 628 additions and 5 deletions
|
@ -241,6 +241,7 @@ var internalPathsNeedingLogin = set.From([]string{
|
||||||
"/.pomerium/jwt",
|
"/.pomerium/jwt",
|
||||||
"/.pomerium/user",
|
"/.pomerium/user",
|
||||||
"/.pomerium/webauthn",
|
"/.pomerium/webauthn",
|
||||||
|
"/.pomerium/api/v1/routes",
|
||||||
})
|
})
|
||||||
|
|
||||||
func (e *Evaluator) evaluateInternal(_ context.Context, req *Request) (*PolicyResponse, error) {
|
func (e *Evaluator) evaluateInternal(_ context.Context, req *Request) (*PolicyResponse, error) {
|
||||||
|
|
|
@ -40,11 +40,32 @@ func (p *Proxy) registerDashboardHandlers(r *mux.Router, opts *config.Options) *
|
||||||
c.Path("/").Handler(httputil.HandlerFunc(p.Callback)).Methods(http.MethodGet)
|
c.Path("/").Handler(httputil.HandlerFunc(p.Callback)).Methods(http.MethodGet)
|
||||||
|
|
||||||
// Programmatic API handlers and middleware
|
// Programmatic API handlers and middleware
|
||||||
a := r.PathPrefix(dashboardPath + "/api").Subrouter()
|
// gorilla mux has a bug that prevents HTTP 405 errors from being returned properly so we do all this manually
|
||||||
// login api handler generates a user-navigable login url to authenticate
|
// https://github.com/gorilla/mux/issues/739
|
||||||
a.Path("/v1/login").Handler(httputil.HandlerFunc(p.ProgrammaticLogin)).
|
r.PathPrefix(dashboardPath + "/api").
|
||||||
Queries(urlutil.QueryRedirectURI, "").
|
Handler(httputil.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
||||||
Methods(http.MethodGet)
|
switch r.URL.Path {
|
||||||
|
// login api handler generates a user-navigable login url to authenticate
|
||||||
|
case dashboardPath + "/api/v1/login":
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !r.URL.Query().Has(urlutil.QueryRedirectURI) {
|
||||||
|
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return p.ProgrammaticLogin(w, r)
|
||||||
|
case dashboardPath + "/api/v1/routes":
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return p.routesPortalJSON(w, r)
|
||||||
|
}
|
||||||
|
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||||
|
return nil
|
||||||
|
}))
|
||||||
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
56
proxy/handlers_portal.go
Normal file
56
proxy/handlers_portal.go
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
package proxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/config"
|
||||||
|
"github.com/pomerium/pomerium/internal/handlers"
|
||||||
|
"github.com/pomerium/pomerium/internal/httputil"
|
||||||
|
"github.com/pomerium/pomerium/proxy/portal"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *Proxy) routesPortalJSON(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
u := p.getUserInfoData(r)
|
||||||
|
rs := p.getPortalRoutes(u)
|
||||||
|
m := map[string]any{}
|
||||||
|
m["routes"] = rs
|
||||||
|
|
||||||
|
b, err := json.Marshal(m)
|
||||||
|
if err != nil {
|
||||||
|
return httputil.NewError(http.StatusInternalServerError, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = w.Write(b)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Proxy) getPortalRoutes(u handlers.UserInfoData) []portal.Route {
|
||||||
|
options := p.currentOptions.Load()
|
||||||
|
pu := p.getPortalUser(u)
|
||||||
|
var routes []*config.Policy
|
||||||
|
for route := range options.GetAllPolicies() {
|
||||||
|
if portal.CheckRouteAccess(pu, route) {
|
||||||
|
routes = append(routes, route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return portal.RoutesFromConfigRoutes(routes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Proxy) getPortalUser(u handlers.UserInfoData) portal.User {
|
||||||
|
pu := portal.User{}
|
||||||
|
pu.SessionID = u.Session.GetId()
|
||||||
|
pu.UserID = u.User.GetId()
|
||||||
|
pu.Email = u.User.GetEmail()
|
||||||
|
for _, dg := range u.DirectoryGroups {
|
||||||
|
if v := dg.ID; v != "" {
|
||||||
|
pu.Groups = append(pu.Groups, dg.ID)
|
||||||
|
}
|
||||||
|
if v := dg.Name; v != "" {
|
||||||
|
pu.Groups = append(pu.Groups, dg.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pu
|
||||||
|
}
|
51
proxy/handlers_portal_test.go
Normal file
51
proxy/handlers_portal_test.go
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
package proxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/config"
|
||||||
|
"github.com/pomerium/pomerium/internal/httputil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestProxy_routesPortalJSON(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
cfg := &config.Config{Options: config.NewDefaultOptions()}
|
||||||
|
to, err := config.ParseWeightedUrls("https://to.example.com")
|
||||||
|
require.NoError(t, err)
|
||||||
|
cfg.Options.Routes = append(cfg.Options.Routes, config.Policy{
|
||||||
|
Name: "public",
|
||||||
|
Description: "PUBLIC ROUTE",
|
||||||
|
LogoURL: "https://logo.example.com",
|
||||||
|
From: "https://from.example.com",
|
||||||
|
To: to,
|
||||||
|
AllowPublicUnauthenticatedAccess: true,
|
||||||
|
})
|
||||||
|
proxy, err := New(ctx, cfg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
r := httptest.NewRequest(http.MethodGet, "/.pomerium/api/v1/routes", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
router := httputil.NewRouter()
|
||||||
|
router = proxy.registerDashboardHandlers(router, cfg.Options)
|
||||||
|
router.ServeHTTP(w, r)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
assert.Equal(t, "application/json", w.Header().Get("Content-Type"))
|
||||||
|
assert.JSONEq(t, `{"routes":[
|
||||||
|
{
|
||||||
|
"id": "4e71df99c0317efb",
|
||||||
|
"name": "public",
|
||||||
|
"from": "https://from.example.com",
|
||||||
|
"type": "http",
|
||||||
|
"description": "PUBLIC ROUTE",
|
||||||
|
"logo_url": "https://logo.example.com"
|
||||||
|
}
|
||||||
|
]}`, w.Body.String())
|
||||||
|
}
|
105
proxy/portal/filter.go
Normal file
105
proxy/portal/filter.go
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
package portal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/config"
|
||||||
|
"github.com/pomerium/pomerium/pkg/policy/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
// User is the computed user information needed for access decisions.
|
||||||
|
type User struct {
|
||||||
|
SessionID string
|
||||||
|
UserID string
|
||||||
|
Email string
|
||||||
|
Groups []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckRouteAccess checks if the user has access to the route.
|
||||||
|
func CheckRouteAccess(user User, route *config.Policy) bool {
|
||||||
|
// check the main policy
|
||||||
|
ppl := route.ToPPL()
|
||||||
|
if checkPPLAccess(user, ppl) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// check sub-policies
|
||||||
|
for _, sp := range route.SubPolicies {
|
||||||
|
if sp.SourcePPL == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ppl, err := parser.New().ParseYAML(strings.NewReader(sp.SourcePPL))
|
||||||
|
if err != nil {
|
||||||
|
// ignore invalid PPL
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if checkPPLAccess(user, ppl) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// nothing matched
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkPPLAccess(user User, ppl *parser.Policy) bool {
|
||||||
|
for _, r := range ppl.Rules {
|
||||||
|
// ignore deny rules
|
||||||
|
if r.Action != parser.ActionAllow {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore complex rules
|
||||||
|
if len(r.Nor) > 0 || len(r.Not) > 0 || len(r.And) > 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
cs := append(append([]parser.Criterion{}, r.Or...), r.And...)
|
||||||
|
for _, c := range cs {
|
||||||
|
ok := checkPPLCriterionAccess(user, c)
|
||||||
|
if ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkPPLCriterionAccess(user User, criterion parser.Criterion) bool {
|
||||||
|
switch criterion.Name {
|
||||||
|
case "accept":
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// require a session
|
||||||
|
if user.SessionID == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch criterion.Name {
|
||||||
|
case "authenticated_user":
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// require a user
|
||||||
|
if user.UserID == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch criterion.Name {
|
||||||
|
case "domain":
|
||||||
|
parts := strings.SplitN(user.Email, "@", 2)
|
||||||
|
return len(parts) == 2 && matchString(parts[1], criterion.Data)
|
||||||
|
case "email":
|
||||||
|
return matchString(user.Email, criterion.Data)
|
||||||
|
case "groups":
|
||||||
|
return matchStringList(user.Groups, criterion.Data)
|
||||||
|
case "user":
|
||||||
|
return matchString(user.UserID, criterion.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
67
proxy/portal/filter_test.go
Normal file
67
proxy/portal/filter_test.go
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
package portal_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/config"
|
||||||
|
"github.com/pomerium/pomerium/pkg/policy/parser"
|
||||||
|
"github.com/pomerium/pomerium/proxy/portal"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCheckRouteAccess(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
u1 := portal.User{}
|
||||||
|
u2 := portal.User{SessionID: "s2", UserID: "u2", Email: "u2@example.com", Groups: []string{"g2"}}
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
user portal.User
|
||||||
|
route *config.Policy
|
||||||
|
}{
|
||||||
|
{"no ppl", u1, &config.Policy{}},
|
||||||
|
{"allow_any_authenticated_user", u1, &config.Policy{AllowAnyAuthenticatedUser: true}},
|
||||||
|
{"allowed_domains", u2, &config.Policy{AllowedDomains: []string{"not.example.com"}}},
|
||||||
|
{"allowed_users", u2, &config.Policy{AllowedUsers: []string{"u3"}}},
|
||||||
|
{"not conditionals", u2, &config.Policy{Policy: mustParsePPL(t, `{"allow": {"not": [{"accept": 1}]}}`)}},
|
||||||
|
{"nor conditionals", u2, &config.Policy{Policy: mustParsePPL(t, `{"allow": {"nor": [{"accept": 1}]}}`)}},
|
||||||
|
{"and conditionals", u2, &config.Policy{Policy: mustParsePPL(t, `{"allow": {"and": [{"accept": 1}, {"accept": 1}]}}`)}},
|
||||||
|
{"authenticated_user", u1, &config.Policy{Policy: mustParsePPL(t, `{"allow": {"or": [{"authenticated_user": 1}]}}`)}},
|
||||||
|
{"domain", u2, &config.Policy{Policy: mustParsePPL(t, `{"allow": {"or": [{"domain": "not.example.com"}]}}`)}},
|
||||||
|
{"email", u1, &config.Policy{Policy: mustParsePPL(t, `{"allow": {"or": [{"email": "u2@example.com"}]}}`)}},
|
||||||
|
{"groups", u2, &config.Policy{Policy: mustParsePPL(t, `{"allow": {"or": [{"groups": {"has": "g3"}}]}}`)}},
|
||||||
|
} {
|
||||||
|
assert.False(t, portal.CheckRouteAccess(tc.user, tc.route), "%s: should deny access for %v to %v",
|
||||||
|
tc.name, tc.user, tc.route)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
user portal.User
|
||||||
|
route *config.Policy
|
||||||
|
}{
|
||||||
|
{"allow_public_unauthenticated_access", u1, &config.Policy{AllowPublicUnauthenticatedAccess: true}},
|
||||||
|
{"allow_any_authenticated_user", u2, &config.Policy{AllowAnyAuthenticatedUser: true}},
|
||||||
|
{"allowed_domains", u2, &config.Policy{AllowedDomains: []string{"example.com"}}},
|
||||||
|
{"allowed_users", u2, &config.Policy{AllowedUsers: []string{"u2"}}},
|
||||||
|
{"and conditionals", u2, &config.Policy{Policy: mustParsePPL(t, `{"allow": {"and": [{"accept": 1}]}}`)}},
|
||||||
|
{"or conditionals", u2, &config.Policy{Policy: mustParsePPL(t, `{"allow": {"or": [{"reject": 1}, {"accept": 1}]}}`)}},
|
||||||
|
{"authenticated_user", u2, &config.Policy{Policy: mustParsePPL(t, `{"allow": {"or": [{"authenticated_user": 1}]}}`)}},
|
||||||
|
{"domain", u2, &config.Policy{Policy: mustParsePPL(t, `{"allow": {"or": [{"domain": "example.com"}]}}`)}},
|
||||||
|
{"email", u2, &config.Policy{Policy: mustParsePPL(t, `{"allow": {"or": [{"email": "u2@example.com"}]}}`)}},
|
||||||
|
{"groups", u2, &config.Policy{Policy: mustParsePPL(t, `{"allow": {"or": [{"groups": {"has": "g2"}}]}}`)}},
|
||||||
|
} {
|
||||||
|
assert.True(t, portal.CheckRouteAccess(tc.user, tc.route), "%s: should grant access for %v to %v",
|
||||||
|
tc.name, tc.user, tc.route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustParsePPL(t testing.TB, raw string) *config.PPLPolicy {
|
||||||
|
ppl, err := parser.New().ParseJSON(strings.NewReader(raw))
|
||||||
|
require.NoError(t, err)
|
||||||
|
return &config.PPLPolicy{Policy: ppl}
|
||||||
|
}
|
108
proxy/portal/matchers.go
Normal file
108
proxy/portal/matchers.go
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
package portal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/pkg/policy/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
type matcher[T any] func(left T, right parser.Value) bool
|
||||||
|
|
||||||
|
var stringMatchers = map[string]matcher[string]{
|
||||||
|
"contains": matchStringContains,
|
||||||
|
"ends_with": matchStringEndsWith,
|
||||||
|
"is": matchStringIs,
|
||||||
|
"starts_with": matchStringStartsWith,
|
||||||
|
}
|
||||||
|
|
||||||
|
var stringListMatchers = map[string]matcher[[]string]{
|
||||||
|
"has": matchStringListHas,
|
||||||
|
"is": matchStringListIs,
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchString(left string, right parser.Value) bool {
|
||||||
|
obj, ok := right.(parser.Object)
|
||||||
|
if !ok {
|
||||||
|
obj = parser.Object{
|
||||||
|
"is": right,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range obj {
|
||||||
|
f, ok := stringMatchers[k]
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
ok = f(left, v)
|
||||||
|
if ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchStringContains(left string, right parser.Value) bool {
|
||||||
|
str, ok := right.(parser.String)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.Contains(left, string(str))
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchStringEndsWith(left string, right parser.Value) bool {
|
||||||
|
str, ok := right.(parser.String)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.HasSuffix(left, string(str))
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchStringIs(left string, right parser.Value) bool {
|
||||||
|
str, ok := right.(parser.String)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return left == string(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchStringStartsWith(left string, right parser.Value) bool {
|
||||||
|
str, ok := right.(parser.String)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.HasPrefix(left, string(str))
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchStringList(left []string, right parser.Value) bool {
|
||||||
|
obj, ok := right.(parser.Object)
|
||||||
|
if !ok {
|
||||||
|
obj = parser.Object{
|
||||||
|
"has": right,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range obj {
|
||||||
|
f, ok := stringListMatchers[k]
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
ok = f(left, v)
|
||||||
|
if ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchStringListHas(left []string, right parser.Value) bool {
|
||||||
|
for _, str := range left {
|
||||||
|
if matchStringIs(str, right) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchStringListIs(left []string, right parser.Value) bool {
|
||||||
|
return len(left) == 1 && matchStringListHas(left, right)
|
||||||
|
}
|
83
proxy/portal/matchers_test.go
Normal file
83
proxy/portal/matchers_test.go
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
package portal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/pkg/policy/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_matchString(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("string", func(t *testing.T) {
|
||||||
|
assert.True(t, matchString("TEST", mustParseValue(t, `"TEST"`)))
|
||||||
|
})
|
||||||
|
t.Run("bool", func(t *testing.T) {
|
||||||
|
assert.False(t, matchString("true", mustParseValue(t, `true`)))
|
||||||
|
})
|
||||||
|
t.Run("number", func(t *testing.T) {
|
||||||
|
assert.False(t, matchString("1", mustParseValue(t, `1`)))
|
||||||
|
})
|
||||||
|
t.Run("null", func(t *testing.T) {
|
||||||
|
assert.False(t, matchString("null", mustParseValue(t, `null`)))
|
||||||
|
})
|
||||||
|
t.Run("array", func(t *testing.T) {
|
||||||
|
assert.False(t, matchString("[]", mustParseValue(t, `[]`)))
|
||||||
|
})
|
||||||
|
t.Run("contains", func(t *testing.T) {
|
||||||
|
assert.True(t, matchString("XYZ", mustParseValue(t, `{"contains":"Y"}`)))
|
||||||
|
assert.False(t, matchString("XYZ", mustParseValue(t, `{"contains":"A"}`)))
|
||||||
|
})
|
||||||
|
t.Run("ends_with", func(t *testing.T) {
|
||||||
|
assert.True(t, matchString("XYZ", mustParseValue(t, `{"ends_with":"Z"}`)))
|
||||||
|
assert.False(t, matchString("XYZ", mustParseValue(t, `{"ends_with":"X"}`)))
|
||||||
|
})
|
||||||
|
t.Run("is", func(t *testing.T) {
|
||||||
|
assert.True(t, matchString("XYZ", mustParseValue(t, `{"is":"XYZ"}`)))
|
||||||
|
assert.False(t, matchString("XYZ", mustParseValue(t, `{"is":"X"}`)))
|
||||||
|
})
|
||||||
|
t.Run("starts_with", func(t *testing.T) {
|
||||||
|
assert.True(t, matchString("XYZ", mustParseValue(t, `{"starts_with":"X"}`)))
|
||||||
|
assert.False(t, matchString("XYZ", mustParseValue(t, `{"starts_with":"Z"}`)))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_matchStringList(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("string", func(t *testing.T) {
|
||||||
|
assert.True(t, matchStringList([]string{"X", "Y", "Z"}, mustParseValue(t, `"Y"`)))
|
||||||
|
assert.False(t, matchStringList([]string{"X", "Y", "Z"}, mustParseValue(t, `"A"`)))
|
||||||
|
})
|
||||||
|
t.Run("bool", func(t *testing.T) {
|
||||||
|
assert.False(t, matchStringList([]string{"true"}, mustParseValue(t, `true`)))
|
||||||
|
})
|
||||||
|
t.Run("number", func(t *testing.T) {
|
||||||
|
assert.False(t, matchStringList([]string{"1"}, mustParseValue(t, `1`)))
|
||||||
|
})
|
||||||
|
t.Run("null", func(t *testing.T) {
|
||||||
|
assert.False(t, matchStringList([]string{"null"}, mustParseValue(t, `null`)))
|
||||||
|
})
|
||||||
|
t.Run("array", func(t *testing.T) {
|
||||||
|
assert.False(t, matchStringList([]string{"[]"}, mustParseValue(t, `[]`)))
|
||||||
|
})
|
||||||
|
t.Run("has", func(t *testing.T) {
|
||||||
|
assert.True(t, matchStringList([]string{"X", "Y", "Z"}, mustParseValue(t, `{"has":"Y"}`)))
|
||||||
|
assert.False(t, matchStringList([]string{"X", "Y", "Z"}, mustParseValue(t, `{"has":"A"}`)))
|
||||||
|
})
|
||||||
|
t.Run("is", func(t *testing.T) {
|
||||||
|
assert.True(t, matchStringList([]string{"X"}, mustParseValue(t, `{"is":"X"}`)))
|
||||||
|
assert.False(t, matchStringList([]string{"X", "Y", "Z"}, mustParseValue(t, `{"is":"Y"}`)))
|
||||||
|
assert.False(t, matchStringList([]string{"X", "Y", "Z"}, mustParseValue(t, `{"is":"A"}`)))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustParseValue(t testing.TB, raw string) parser.Value {
|
||||||
|
v, err := parser.ParseValue(strings.NewReader(raw))
|
||||||
|
require.NoError(t, err)
|
||||||
|
return v
|
||||||
|
}
|
60
proxy/portal/portal.go
Normal file
60
proxy/portal/portal.go
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
// Package portal contains the code for the routes portal
|
||||||
|
package portal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/config"
|
||||||
|
"github.com/pomerium/pomerium/internal/urlutil"
|
||||||
|
"github.com/pomerium/pomerium/pkg/zero/importutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Route is a portal route.
|
||||||
|
type Route struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
From string `json:"from"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
ConnectCommand string `json:"connect_command,omitempty"`
|
||||||
|
LogoURL string `json:"logo_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoutesFromConfigRoutes converts config routes into portal routes.
|
||||||
|
func RoutesFromConfigRoutes(routes []*config.Policy) []Route {
|
||||||
|
prs := make([]Route, len(routes))
|
||||||
|
for i, route := range routes {
|
||||||
|
pr := Route{}
|
||||||
|
pr.ID = route.ID
|
||||||
|
if pr.ID == "" {
|
||||||
|
pr.ID = fmt.Sprintf("%x", route.MustRouteID())
|
||||||
|
}
|
||||||
|
pr.Name = route.Name
|
||||||
|
pr.From = route.From
|
||||||
|
fromURL, err := urlutil.ParseAndValidateURL(route.From)
|
||||||
|
if err == nil {
|
||||||
|
if strings.HasPrefix(fromURL.Scheme, "tcp+") {
|
||||||
|
pr.Type = "tcp"
|
||||||
|
pr.ConnectCommand = "pomerium-cli tcp " + fromURL.Host
|
||||||
|
} else if strings.HasPrefix(fromURL.Scheme, "udp+") {
|
||||||
|
pr.Type = "udp"
|
||||||
|
pr.ConnectCommand = "pomerium-cli udp " + fromURL.Host
|
||||||
|
} else {
|
||||||
|
pr.Type = "http"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pr.Type = "http"
|
||||||
|
}
|
||||||
|
pr.Description = route.Description
|
||||||
|
pr.LogoURL = route.LogoURL
|
||||||
|
prs[i] = pr
|
||||||
|
}
|
||||||
|
// generate names if they're empty
|
||||||
|
for i, name := range importutil.GenerateRouteNames(routes) {
|
||||||
|
if prs[i].Name == "" {
|
||||||
|
prs[i].Name = name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return prs
|
||||||
|
}
|
71
proxy/portal/portal_test.go
Normal file
71
proxy/portal/portal_test.go
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
package portal_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/config"
|
||||||
|
"github.com/pomerium/pomerium/proxy/portal"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRouteFromConfigRoute(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
to1, err := config.ParseWeightedUrls("https://to.example.com")
|
||||||
|
require.NoError(t, err)
|
||||||
|
to2, err := config.ParseWeightedUrls("tcp://postgres:5432")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, []portal.Route{
|
||||||
|
{
|
||||||
|
ID: "4e71df99c0317efb",
|
||||||
|
Name: "from",
|
||||||
|
Type: "http",
|
||||||
|
From: "https://from.example.com",
|
||||||
|
Description: "ROUTE #1",
|
||||||
|
LogoURL: "https://logo.example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "7c377f11cdb9700e",
|
||||||
|
Name: "from-path",
|
||||||
|
Type: "http",
|
||||||
|
From: "https://from.example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "708e3cbd0bbe8547",
|
||||||
|
Name: "postgres",
|
||||||
|
Type: "tcp",
|
||||||
|
From: "tcp+https://postgres.example.com:5432",
|
||||||
|
ConnectCommand: "pomerium-cli tcp postgres.example.com:5432",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "2dd08d87486e051a",
|
||||||
|
Name: "dns",
|
||||||
|
Type: "udp",
|
||||||
|
From: "udp+https://dns.example.com:53",
|
||||||
|
ConnectCommand: "pomerium-cli udp dns.example.com:53",
|
||||||
|
},
|
||||||
|
}, portal.RoutesFromConfigRoutes([]*config.Policy{
|
||||||
|
{
|
||||||
|
From: "https://from.example.com",
|
||||||
|
To: to1,
|
||||||
|
Description: "ROUTE #1",
|
||||||
|
LogoURL: "https://logo.example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
From: "https://from.example.com",
|
||||||
|
To: to1,
|
||||||
|
Path: "/path",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
From: "tcp+https://postgres.example.com:5432",
|
||||||
|
To: to2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
From: "udp+https://dns.example.com:53",
|
||||||
|
To: to2,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue