Skip to content

Commit a69457d

Browse files
committed
Pass X-Openrun-User and X-Openrun-Perm headers in proxy
1 parent 0fd4956 commit a69457d

File tree

7 files changed

+189
-27
lines changed

7 files changed

+189
-27
lines changed

internal/app/app.go

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,9 @@ type App struct {
8787
lastRequestTime atomic.Int64
8888
secretEvalFunc func([][]string, string, string) (string, error)
8989
auditInsert func(*types.AuditEvent) error
90-
AppRunPath string // path to the app run directory
91-
authorizer types.AuthorizerFunc // the authorizer function to use, can be null
90+
AppRunPath string // path to the app run directory
91+
authorizer types.AuthorizerFunc // the authorizer function to use, can be null
92+
customPermsFunc types.CustomPermsFunc // the custom permissions function to use, can be null
9293
}
9394

9495
type starlarkCacheEntry struct {
@@ -105,19 +106,21 @@ func NewApp(sourceFS *appfs.SourceFs, workFS *appfs.WorkFs, logger *types.Logger
105106
appEntry *types.AppEntry, systemConfig *types.SystemConfig,
106107
plugins map[string]types.PluginSettings, appConfig types.AppConfig, notifyClose chan<- types.AppPathDomain,
107108
secretEvalFunc func([][]string, string, string) (string, error),
108-
auditInsert func(*types.AuditEvent) error, serverConfig *types.ServerConfig, authorizer types.AuthorizerFunc) (*App, error) {
109+
auditInsert func(*types.AuditEvent) error, serverConfig *types.ServerConfig,
110+
authorizer types.AuthorizerFunc, customPermsFunc types.CustomPermsFunc) (*App, error) {
109111
newApp := &App{
110-
sourceFS: sourceFS,
111-
Logger: logger,
112-
AppEntry: appEntry,
113-
systemConfig: systemConfig,
114-
starlarkCache: map[string]*starlarkCacheEntry{},
115-
notifyClose: notifyClose,
116-
secretEvalFunc: secretEvalFunc,
117-
appStyle: &dev.AppStyle{},
118-
auditInsert: auditInsert,
119-
serverConfig: serverConfig,
120-
authorizer: authorizer,
112+
sourceFS: sourceFS,
113+
Logger: logger,
114+
AppEntry: appEntry,
115+
systemConfig: systemConfig,
116+
starlarkCache: map[string]*starlarkCacheEntry{},
117+
notifyClose: notifyClose,
118+
secretEvalFunc: secretEvalFunc,
119+
appStyle: &dev.AppStyle{},
120+
auditInsert: auditInsert,
121+
serverConfig: serverConfig,
122+
authorizer: authorizer,
123+
customPermsFunc: customPermsFunc,
121124
}
122125
newApp.plugins = NewAppPlugins(newApp, plugins, appEntry.Metadata.Accounts)
123126
newApp.AppConfig = appConfig

internal/app/setup.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -853,6 +853,29 @@ func (a *App) addProxyConfig(count int, router *chi.Mux, proxyDef *starlarkstruc
853853
r.Header.Set("X-Forwarded-Prefix", a.Path)
854854
}
855855

856+
for key := range r.Header {
857+
// Delete all x-openrun- prefixed headers
858+
if strings.HasPrefix(strings.ToLower(key), "x-openrun-") {
859+
r.Header.Del(key)
860+
}
861+
}
862+
863+
customPerms := make([]string, 0)
864+
if a.customPermsFunc != nil {
865+
customPerms, err = a.customPermsFunc(r.Context())
866+
}
867+
// Add the user and custom permissions to the request headers
868+
r.Header.Set(types.OPENRUN_HEADER_PERMS, strings.Join(customPerms, ","))
869+
870+
// Get user from context, use anonymous user if not set
871+
userId := types.ANONYMOUS_USER
872+
if userVal := r.Context().Value(types.USER_ID); userVal != nil {
873+
if userStr, ok := userVal.(string); ok {
874+
userId = userStr
875+
}
876+
}
877+
r.Header.Set(types.OPENRUN_HEADER_USER, userId)
878+
856879
// Set the response headers
857880
for key, value := range responseHeaders {
858881
if value == nil {

internal/app/tests/app_test_helper.go

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,48 +22,54 @@ import (
2222
)
2323

2424
func CreateDevModeTestApp(logger *types.Logger, fileData map[string]string) (*app.App, *appfs.WorkFs, error) {
25-
return CreateTestAppInt(logger, "/test", fileData, true, nil, nil, nil, "app_dev_testapp", types.AppSettings{}, nil, nil)
25+
return CreateTestAppInt(logger, "/test", fileData, true, nil, nil, nil, "app_dev_testapp", types.AppSettings{}, nil, nil, nil, nil)
2626
}
2727

2828
func CreateTestApp(logger *types.Logger, fileData map[string]string) (*app.App, *appfs.WorkFs, error) {
29-
return CreateTestAppInt(logger, "/test", fileData, false, nil, nil, nil, "app_prd_testapp", types.AppSettings{}, nil, nil)
29+
return CreateTestAppInt(logger, "/test", fileData, false, nil, nil, nil, "app_prd_testapp", types.AppSettings{}, nil, nil, nil, nil)
3030
}
3131

3232
func CreateTestAppConfig(logger *types.Logger, fileData map[string]string, appConfig types.AppConfig) (*app.App, *appfs.WorkFs, error) {
33-
return CreateTestAppInt(logger, "/test", fileData, false, nil, nil, nil, "app_prd_testapp", types.AppSettings{}, nil, &appConfig)
33+
return CreateTestAppInt(logger, "/test", fileData, false, nil, nil, nil, "app_prd_testapp", types.AppSettings{}, nil, &appConfig, nil, nil)
3434
}
3535

3636
func CreateTestAppParams(logger *types.Logger, fileData map[string]string, params map[string]string) (*app.App, *appfs.WorkFs, error) {
37-
return CreateTestAppInt(logger, "/test", fileData, false, nil, nil, nil, "app_prd_testapp", types.AppSettings{}, params, nil)
37+
return CreateTestAppInt(logger, "/test", fileData, false, nil, nil, nil, "app_prd_testapp", types.AppSettings{}, params, nil, nil, nil)
3838
}
3939

4040
func CreateTestAppRoot(logger *types.Logger, fileData map[string]string) (*app.App, *appfs.WorkFs, error) {
41-
return CreateTestAppInt(logger, "/", fileData, false, nil, nil, nil, "app_prd_testapp", types.AppSettings{}, nil, nil)
41+
return CreateTestAppInt(logger, "/", fileData, false, nil, nil, nil, "app_prd_testapp", types.AppSettings{}, nil, nil, nil, nil)
4242
}
4343

4444
func CreateTestAppPlugin(logger *types.Logger, fileData map[string]string,
4545
plugins []string, permissions []types.Permission, pluginConfig map[string]types.PluginSettings) (*app.App, *appfs.WorkFs, error) {
46-
return CreateTestAppInt(logger, "/test", fileData, false, plugins, permissions, pluginConfig, "app_prd_testapp", types.AppSettings{}, nil, nil)
46+
return CreateTestAppInt(logger, "/test", fileData, false, plugins, permissions, pluginConfig, "app_prd_testapp", types.AppSettings{}, nil, nil, nil, nil)
4747
}
4848

4949
func CreateTestAppPluginRoot(logger *types.Logger, fileData map[string]string,
5050
plugins []string, permissions []types.Permission, pluginConfig map[string]types.PluginSettings) (*app.App, *appfs.WorkFs, error) {
51-
return CreateTestAppInt(logger, "/", fileData, false, plugins, permissions, pluginConfig, "app_prd_testapp", types.AppSettings{}, nil, nil)
51+
return CreateTestAppInt(logger, "/", fileData, false, plugins, permissions, pluginConfig, "app_prd_testapp", types.AppSettings{}, nil, nil, nil, nil)
5252
}
5353

5454
func CreateDevAppPlugin(logger *types.Logger, fileData map[string]string, plugins []string,
5555
permissions []types.Permission, pluginConfig map[string]types.PluginSettings) (*app.App, *appfs.WorkFs, error) {
56-
return CreateTestAppInt(logger, "/test", fileData, true, plugins, permissions, pluginConfig, "app_dev_testapp", types.AppSettings{}, nil, nil)
56+
return CreateTestAppInt(logger, "/test", fileData, true, plugins, permissions, pluginConfig, "app_dev_testapp", types.AppSettings{}, nil, nil, nil, nil)
5757
}
5858

5959
func CreateTestAppPluginId(logger *types.Logger, fileData map[string]string,
6060
plugins []string, permissions []types.Permission, pluginConfig map[string]types.PluginSettings, id string, settings types.AppSettings) (*app.App, *appfs.WorkFs, error) {
61-
return CreateTestAppInt(logger, "/test", fileData, false, plugins, permissions, pluginConfig, id, settings, nil, nil)
61+
return CreateTestAppInt(logger, "/test", fileData, false, plugins, permissions, pluginConfig, id, settings, nil, nil, nil, nil)
62+
}
63+
64+
func CreateTestAppAuthorizer(logger *types.Logger, fileData map[string]string,
65+
plugins []string, permissions []types.Permission, pluginConfig map[string]types.PluginSettings, authorizer types.AuthorizerFunc, customPermsFunc types.CustomPermsFunc) (*app.App, *appfs.WorkFs, error) {
66+
return CreateTestAppInt(logger, "/test", fileData, false, plugins, permissions, pluginConfig, "app_prd_testapp", types.AppSettings{}, nil, nil, authorizer, customPermsFunc)
6267
}
6368

6469
func CreateTestAppInt(logger *types.Logger, path string, fileData map[string]string, isDev bool,
6570
plugins []string, permissions []types.Permission, pluginConfig map[string]types.PluginSettings,
66-
id string, settings types.AppSettings, params map[string]string, appConfig *types.AppConfig) (*app.App, *appfs.WorkFs, error) {
71+
id string, settings types.AppSettings, params map[string]string, appConfig *types.AppConfig,
72+
authorizer types.AuthorizerFunc, customPermsFunc types.CustomPermsFunc) (*app.App, *appfs.WorkFs, error) {
6773
systemConfig := types.SystemConfig{TailwindCSSCommand: "", AllowedEnv: []string{"HOME"}}
6874
var fs appfs.ReadableFS
6975
if isDev {
@@ -107,7 +113,7 @@ func CreateTestAppInt(logger *types.Logger, path string, fileData map[string]str
107113
workFS := appfs.NewWorkFs("", &TestWriteFS{TestReadFS: &TestReadFS{fileData: map[string]string{}}})
108114
a, err := app.NewApp(sourceFS, workFS, logger,
109115
createTestAppEntry(id, path, isDev, metadata), &systemConfig, pluginConfig, *appConfig,
110-
nil, secretManager.AppEvalTemplate, nil, &types.ServerConfig{}, nil)
116+
nil, secretManager.AppEvalTemplate, nil, &types.ServerConfig{}, authorizer, customPermsFunc)
111117
if err != nil {
112118
return nil, nil, err
113119
}

internal/app/tests/proxy_test.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package app_test
55

66
import (
7+
"context"
78
"fmt"
89
"io"
910
"net/http"
@@ -653,3 +654,116 @@ permissions=[
653654
testutil.AssertEqualsString(t, "header", "NEWVAL", response.Header().Get("NEWH"))
654655
testutil.AssertEqualsString(t, "header", "aa/abc/defbb", response.Header().Get("NEWTEMP"))
655656
}
657+
658+
func TestProxyUserAndPermsHeaders(t *testing.T) {
659+
// Test that X-Openrun-User and X-Openrun-Perms headers are passed to proxied endpoint
660+
var receivedUser string
661+
var receivedPerms string
662+
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
663+
receivedUser = r.Header.Get("X-Openrun-User")
664+
receivedPerms = r.Header.Get("X-Openrun-Perms")
665+
io.WriteString(w, "test contents") //nolint:errcheck
666+
}))
667+
668+
logger := testutil.TestLogger()
669+
fileData := map[string]string{
670+
"app.star": fmt.Sprintf(`
671+
load("proxy.in", "proxy")
672+
673+
app = ace.app("testApp", routes = [ace.proxy("/", proxy.config("%s"))],
674+
permissions=[
675+
ace.permission("proxy.in", "config"),
676+
]
677+
)`, testServer.URL),
678+
}
679+
680+
// Create custom authorizer and perms func
681+
authorizer := func(ctx context.Context, permissions []string) (bool, error) {
682+
// Always allow
683+
return true, nil
684+
}
685+
686+
customPermsFunc := func(ctx context.Context) ([]string, error) {
687+
// Return custom permissions
688+
return []string{"read:data", "write:data", "admin"}, nil
689+
}
690+
691+
a, _, err := CreateTestAppAuthorizer(logger, fileData, []string{"proxy.in"},
692+
[]types.Permission{
693+
{Plugin: "proxy.in", Method: "config"},
694+
}, map[string]types.PluginSettings{}, authorizer, customPermsFunc)
695+
if err != nil {
696+
t.Fatalf("Error %s", err)
697+
}
698+
699+
request := httptest.NewRequest("GET", "/test/abc", nil)
700+
// Set user ID in context as it would be set by the server middleware
701+
ctx := context.WithValue(request.Context(), types.USER_ID, types.ANONYMOUS_USER)
702+
request = request.WithContext(ctx)
703+
response := httptest.NewRecorder()
704+
a.ServeHTTP(response, request)
705+
706+
testutil.AssertEqualsInt(t, "code", 200, response.Code)
707+
testutil.AssertEqualsString(t, "body", "test contents", response.Body.String())
708+
709+
// Verify the headers were passed to the proxied endpoint
710+
testutil.AssertEqualsString(t, "X-Openrun-User", types.ANONYMOUS_USER, receivedUser)
711+
testutil.AssertEqualsString(t, "X-Openrun-Perms", "read:data,write:data,admin", receivedPerms)
712+
}
713+
714+
func TestProxyUserHeaderWithAuthentication(t *testing.T) {
715+
// Test that X-Openrun-User header contains the authenticated user
716+
var receivedUser string
717+
var receivedExtra string
718+
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
719+
receivedUser = r.Header.Get("X-Openrun-User")
720+
receivedExtra = r.Header.Get("X-Openrun-Extra")
721+
io.WriteString(w, "test contents") //nolint:errcheck
722+
}))
723+
724+
logger := testutil.TestLogger()
725+
fileData := map[string]string{
726+
"app.star": fmt.Sprintf(`
727+
load("proxy.in", "proxy")
728+
729+
app = ace.app("testApp", routes = [ace.proxy("/", proxy.config("%s"))],
730+
permissions=[
731+
ace.permission("proxy.in", "config"),
732+
]
733+
)`, testServer.URL),
734+
}
735+
736+
// Create custom authorizer that sets a user in context
737+
authorizer := func(ctx context.Context, permissions []string) (bool, error) {
738+
// Always allow
739+
return true, nil
740+
}
741+
742+
customPermsFunc := func(ctx context.Context) ([]string, error) {
743+
// Return empty custom permissions
744+
return []string{}, nil
745+
}
746+
747+
a, _, err := CreateTestAppAuthorizer(logger, fileData, []string{"proxy.in"},
748+
[]types.Permission{
749+
{Plugin: "proxy.in", Method: "config"},
750+
}, map[string]types.PluginSettings{}, authorizer, customPermsFunc)
751+
if err != nil {
752+
t.Fatalf("Error %s", err)
753+
}
754+
755+
request := httptest.NewRequest("GET", "/test/abc", nil)
756+
request.Header.Set("X-Openrun-Extra", "testvalue")
757+
// Set authenticated user in context as it would be set by the server middleware
758+
ctx := context.WithValue(request.Context(), types.USER_ID, "testuser@example.com")
759+
request = request.WithContext(ctx)
760+
response := httptest.NewRecorder()
761+
a.ServeHTTP(response, request)
762+
763+
testutil.AssertEqualsInt(t, "code", 200, response.Code)
764+
testutil.AssertEqualsString(t, "body", "test contents", response.Body.String())
765+
766+
// Verify the user header was passed to the proxied endpoint
767+
testutil.AssertEqualsString(t, "X-Openrun-User", "testuser@example.com", receivedUser)
768+
testutil.AssertEqualsString(t, "X-Openrun-Extra", "", receivedExtra)
769+
}

internal/server/app_apis.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,7 @@ func (s *Server) setupApp(appEntry *types.AppEntry, tx types.Transaction) (*app.
367367
})
368368
return app.NewApp(sourceFS, workFS, &appLogger, appEntry, &s.config.System,
369369
s.config.Plugins, s.config.AppConfig, s.notifyClose, s.secretsManager.AppEvalTemplate,
370-
s.InsertAuditEvent, s.config, s.AuthorizeAny)
370+
s.InsertAuditEvent, s.config, s.AuthorizeAny, s.GetCustomPermissions)
371371
}
372372

373373
func (s *Server) GetAppApi(ctx context.Context, appPath string) (*types.AppGetResponse, error) {

internal/server/server.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -845,7 +845,7 @@ func (s *Server) GetListAppsApp() (*app.App, error) {
845845
appLogger := types.Logger{Logger: &subLogger}
846846
s.listAppsApp, err = app.NewApp(sourceFS, nil, &appLogger, &appEntry, &s.config.System,
847847
s.config.Plugins, s.config.AppConfig, s.notifyClose, s.secretsManager.AppEvalTemplate,
848-
s.InsertAuditEvent, s.config, s.AuthorizeAny)
848+
s.InsertAuditEvent, s.config, s.AuthorizeAny, s.GetCustomPermissions)
849849
if err != nil {
850850
return nil, err
851851
}
@@ -896,6 +896,14 @@ func (s *Server) Authorize(ctx context.Context, permission types.RBACPermission,
896896
return s.rbacManager.Authorize(userId, appPathDomain, appAuth, permission, groups, isAppLevelPermission)
897897
}
898898

899+
func (s *Server) GetCustomPermissions(ctx context.Context) ([]string, error) {
900+
userId := ctx.Value(types.USER_ID).(string)
901+
groups := ctx.Value(types.GROUPS).([]string)
902+
appPathDomain := ctx.Value(types.APP_PATH_DOMAIN).(types.AppPathDomain)
903+
appAuth := string(ctx.Value(types.APP_AUTH).(types.AppAuthnType))
904+
return s.rbacManager.GetCustomPermissions(userId, appPathDomain, appAuth, groups)
905+
}
906+
899907
// AuthorizeList checks if the user has access to perform list operation on the specified app
900908
// For RBAC mode, uses RBAC permissions. For non-RBAC mode, look at whether app is using
901909
// same authentication types as used by the caller

internal/types/types.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -723,6 +723,7 @@ const (
723723
)
724724

725725
type AuthorizerFunc func(ctx context.Context, permissions []string) (bool, error)
726+
type CustomPermsFunc func(ctx context.Context) ([]string, error)
726727

727728
type SAMLConfig struct {
728729
MetadataURL string `toml:"metadata_url"`
@@ -740,3 +741,10 @@ const (
740741
COOKIE_SESSION_SECRET_KV = "cookie_session_secret"
741742
COOKIE_SESSION_BLOCK_KEY_KV = "cookie_session_block_key"
742743
)
744+
745+
const (
746+
// OpenRun headers are used to pass information to the downstream service
747+
OPENRUN_HEADER_PREFIX = "X-Openrun-"
748+
OPENRUN_HEADER_USER = OPENRUN_HEADER_PREFIX + "User"
749+
OPENRUN_HEADER_PERMS = OPENRUN_HEADER_PREFIX + "Perms"
750+
)

0 commit comments

Comments
 (0)