Skip to content

Commit 1fb5c64

Browse files
Merge pull request #14701 from jhadvig/OCPBUGS-49291
OCPBUGS-49291: Improving the DevExp for passing the CSP directives to console per flag + Make use of connect-src and object-src directives
2 parents f6d63b5 + 39d031f commit 1fb5c64

File tree

8 files changed

+147
-154
lines changed

8 files changed

+147
-154
lines changed

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,23 @@ this way, then 'none' will be used. Additionally, violation reporting is throttl
461461
spamming the telemetry service with repetitive data. Identical violations will not be
462462
reported more than once a day.
463463

464+
In case of local developement of the dynamic plugin, just pass needed CSP directives address to the console server, using the `--content-security-policy` flag.
465+
466+
Example:
467+
468+
```
469+
./bin/bridge --content-security-policy script-src='localhost:1234',font-src='localhost:2345 localhost:3456'
470+
```
471+
472+
List of configurable CSP directives is available in the [openshift/api repository](https://github.com/openshift/api/blob/master/console/v1/types_console_plugin.go#L102-L137).
473+
474+
The list is extended automatically by the console server with following CSP directives:
475+
- `"frame-src 'none'"`
476+
- `"frame-ancestors 'none'"`
477+
- `"object-src 'none'"`
478+
479+
Currently this feature is behind feature gate.
480+
464481
## Frontend Packages
465482
- [console-dynamic-plugin-sdk](./frontend/packages/console-dynamic-plugin-sdk/README.md)
466483
[[API]](./frontend/packages/console-dynamic-plugin-sdk/docs/api.md)

cmd/bridge/main.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,10 @@ func main() {
130130
fs.Var(&consolePluginsFlags, "plugins", "List of plugin entries that are enabled for the console. Each entry consist of plugin-name as a key and plugin-endpoint as a value.")
131131
fPluginProxy := fs.String("plugin-proxy", "", "Defines various service types to which will console proxy plugins requests. (JSON as string)")
132132
fI18NamespacesFlags := fs.String("i18n-namespaces", "", "List of namespaces separated by comma. Example --i18n-namespaces=plugin__acm,plugin__kubevirt")
133+
133134
fContentSecurityPolicyEnabled := fs.Bool("content-security-policy-enabled", false, "Flag to indicate if Content Secrity Policy features should be enabled.")
134-
fContentSecurityPolicy := fs.String("content-security-policy", "", "Content security policy for the console. (JSON as string)")
135+
consoleCSPFlags := serverconfig.MultiKeyValue{}
136+
fs.Var(&consoleCSPFlags, "content-security-policy", "List of CSP directives that are enabled for the console. Each entry consist of csp-directive-name as a key and csp-directive-value as a value. Example --content-security-policy script-src='localhost:9000',font-src='localhost:9001'")
135137

136138
telemetryFlags := serverconfig.MultiKeyValue{}
137139
fs.Var(&telemetryFlags, "telemetry", "Telemetry configuration that can be used by console plugins. Each entry should be a key=value pair.")
@@ -282,8 +284,8 @@ func main() {
282284
EnabledConsolePlugins: consolePluginsFlags,
283285
I18nNamespaces: i18nNamespaces,
284286
PluginProxy: *fPluginProxy,
285-
ContentSecurityPolicy: *fContentSecurityPolicy,
286287
ContentSecurityPolicyEnabled: *fContentSecurityPolicyEnabled,
288+
ContentSecurityPolicy: consoleCSPFlags,
287289
QuickStarts: *fQuickStarts,
288290
AddPage: *fAddPage,
289291
ProjectAccessClusterRoles: *fProjectAccessClusterRoles,

pkg/server/server.go

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -161,8 +161,8 @@ type Server struct {
161161
ClusterManagementProxyConfig *proxy.Config
162162
CookieEncryptionKey []byte
163163
CookieAuthenticationKey []byte
164-
ContentSecurityPolicy string
165164
ContentSecurityPolicyEnabled bool
165+
ContentSecurityPolicy serverconfig.MultiKeyValue
166166
ControlPlaneTopology string
167167
CopiedCSVsDisabled bool
168168
CSRFVerifier *csrfverifier.CSRFVerifier
@@ -281,8 +281,7 @@ func (s *Server) HTTPHandler() (http.Handler, error) {
281281
tpl.Delims("[[", "]]")
282282
tpls, err := tpl.ParseFiles(path.Join(s.PublicDir, tokenizerPageTemplateName))
283283
if err != nil {
284-
fmt.Printf("%v not found in configured public-dir path: %v", tokenizerPageTemplateName, err)
285-
os.Exit(1)
284+
klog.Fatalf("%v not found in configured public-dir path: %v", tokenizerPageTemplateName, err)
286285
}
287286

288287
if err := tpls.ExecuteTemplate(w, tokenizerPageTemplateName, templateData); err != nil {
@@ -542,12 +541,10 @@ func (s *Server) HTTPHandler() (http.Handler, error) {
542541
proxyConfig, err := plugins.ParsePluginProxyConfig(s.PluginProxy)
543542
if err != nil {
544543
klog.Fatalf("Error parsing plugin proxy config: %s", err)
545-
os.Exit(1)
546544
}
547545
proxyServiceHandlers, err := plugins.GetPluginProxyServiceHandlers(proxyConfig, s.PluginsProxyTLSConfig, pluginProxyEndpoint)
548546
if err != nil {
549547
klog.Fatalf("Error getting plugin proxy handlers: %s", err)
550-
os.Exit(1)
551548
}
552549
if len(proxyServiceHandlers) != 0 {
553550
klog.Infoln("The following console endpoints are now proxied to these services:")
@@ -586,7 +583,7 @@ func (s *Server) HTTPHandler() (http.Handler, error) {
586583
ConsoleCommit: os.Getenv("SOURCE_GIT_COMMIT"),
587584
Plugins: pluginsHandler.GetPluginsList(),
588585
Capabilities: s.Capabilities,
589-
ContentSecurityPolicy: s.ContentSecurityPolicy,
586+
ContentSecurityPolicy: s.ContentSecurityPolicy.String(),
590587
})
591588
}))
592589

@@ -707,7 +704,6 @@ func (s *Server) indexHandler(w http.ResponseWriter, r *http.Request) {
707704
)
708705
if err != nil {
709706
klog.Fatalf("Error building Content Security Policy directives: %s", err)
710-
os.Exit(1)
711707
}
712708
w.Header().Set("Content-Security-Policy-Report-Only", strings.Join(cspDirectives, "; "))
713709
}
@@ -791,8 +787,7 @@ func (s *Server) indexHandler(w http.ResponseWriter, r *http.Request) {
791787
tpl.Delims("[[", "]]")
792788
tpls, err := tpl.ParseFiles(path.Join(s.PublicDir, indexPageTemplateName))
793789
if err != nil {
794-
fmt.Printf("index.html not found in configured public-dir path: %v", err)
795-
os.Exit(1)
790+
klog.Fatalf("index.html not found in configured public-dir path: %v", err)
796791
}
797792

798793
if err := tpls.ExecuteTemplate(w, indexPageTemplateName, templateData); err != nil {

pkg/serverconfig/config.go

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ func SetFlagsFromConfig(fs *flag.FlagSet, config *Config) (err error) {
176176
if err != nil {
177177
return err
178178
}
179+
179180
addContentSecurityPolicyEnabled(fs, &config.ContentSecurityPolicyEnabled)
180181
addContentSecurityPolicy(fs, config.ContentSecurityPolicy)
181182
addTelemetry(fs, config.Telemetry)
@@ -418,6 +419,37 @@ func isAlreadySet(fs *flag.FlagSet, name string) bool {
418419
return alreadySet
419420
}
420421

422+
func addContentSecurityPolicy(fs *flag.FlagSet, csp MultiKeyValue) {
423+
for cspDirectiveName, cspDirectiveValue := range csp {
424+
directiveName := getDirectiveName(cspDirectiveName)
425+
if directiveName == "" {
426+
klog.Fatalf("invalid CSP directive: %s", cspDirectiveName)
427+
}
428+
429+
fs.Set("content-security-policy", fmt.Sprintf("%s=%s", directiveName, cspDirectiveValue))
430+
}
431+
}
432+
433+
func getDirectiveName(directive string) string {
434+
switch directive {
435+
case string(consolev1.DefaultSrc):
436+
return "default-src"
437+
case string(consolev1.ImgSrc):
438+
return "img-src"
439+
case string(consolev1.FontSrc):
440+
return "font-src"
441+
case string(consolev1.ScriptSrc):
442+
return "script-src"
443+
case string(consolev1.StyleSrc):
444+
return "style-src"
445+
case string(consolev1.ConnectSrc):
446+
return "connect-src"
447+
default:
448+
klog.Infof("ignored invalid CSP directive: %s", directive)
449+
return ""
450+
}
451+
}
452+
421453
func addPlugins(fs *flag.FlagSet, plugins MultiKeyValue) {
422454
for pluginName, pluginEndpoint := range plugins {
423455
fs.Set("plugins", fmt.Sprintf("%s=%s", pluginName, pluginEndpoint))
@@ -434,18 +466,6 @@ func addI18nNamespaces(fs *flag.FlagSet, i18nNamespaces []string) {
434466
fs.Set("i18n-namespaces", strings.Join(i18nNamespaces, ","))
435467
}
436468

437-
func addContentSecurityPolicy(fs *flag.FlagSet, csp map[consolev1.DirectiveType][]string) error {
438-
if csp != nil {
439-
marshaledCSP, err := json.Marshal(csp)
440-
if err != nil {
441-
klog.Fatalf("Could not marshal ConsoleConfig 'content-security-policy' field: %v", err)
442-
return err
443-
}
444-
fs.Set("content-security-policy", string(marshaledCSP))
445-
}
446-
return nil
447-
}
448-
449469
func addContentSecurityPolicyEnabled(fs *flag.FlagSet, enabled *bool) {
450470
if enabled != nil && *enabled {
451471
fs.Set("content-security-policy-enabled", "true")

pkg/serverconfig/config_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,13 +289,29 @@ func TestSetFlagsFromConfig(t *testing.T) {
289289
},
290290
expectedError: nil,
291291
},
292+
{
293+
name: "Should apply CSP configuration",
294+
config: Config{
295+
APIVersion: "console.openshift.io/v1",
296+
Kind: "ConsoleConfig",
297+
ContentSecurityPolicy: MultiKeyValue{
298+
"FontSrc": "value2 value3",
299+
"ScriptSrc": "value1",
300+
},
301+
},
302+
expectedFlagValues: map[string]string{
303+
"content-security-policy": "font-src=value2 value3, script-src=value1",
304+
},
305+
expectedError: nil,
306+
},
292307
}
293308
for _, test := range tests {
294309
t.Run(test.name, func(t *testing.T) {
295310
fs := &flag.FlagSet{}
296311
fs.String("config", "", "")
297312
fs.Var(&MultiKeyValue{}, "plugins", "")
298313
fs.Var(&MultiKeyValue{}, "telemetry", "")
314+
fs.Var(&MultiKeyValue{}, "content-security-policy", "")
299315

300316
actualError := SetFlagsFromConfig(fs, &test.config)
301317
actual := make(map[string]string)

pkg/serverconfig/types.go

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package serverconfig
22

33
import (
44
configv1 "github.com/openshift/api/config/v1"
5-
v1 "github.com/openshift/api/console/v1"
65
operatorv1 "github.com/openshift/api/operator/v1"
76
authorizationv1 "k8s.io/api/authorization/v1"
87
)
@@ -23,12 +22,12 @@ type Config struct {
2322
Providers `yaml:"providers"`
2423
Helm `yaml:"helm"`
2524
MonitoringInfo `yaml:"monitoringInfo,omitempty"`
26-
Plugins MultiKeyValue `yaml:"plugins,omitempty"`
27-
I18nNamespaces []string `yaml:"i18nNamespaces,omitempty"`
28-
Proxy Proxy `yaml:"proxy,omitempty"`
29-
ContentSecurityPolicyEnabled bool `yaml:"contentSecurityPolicyEnabled,omitempty"`
30-
ContentSecurityPolicy map[v1.DirectiveType][]string `yaml:"contentSecurityPolicy,omitempty"`
31-
Telemetry MultiKeyValue `yaml:"telemetry,omitempty"`
25+
Plugins MultiKeyValue `yaml:"plugins,omitempty"`
26+
I18nNamespaces []string `yaml:"i18nNamespaces,omitempty"`
27+
Proxy Proxy `yaml:"proxy,omitempty"`
28+
ContentSecurityPolicyEnabled bool `yaml:"contentSecurityPolicyEnabled,omitempty"`
29+
ContentSecurityPolicy MultiKeyValue `yaml:"contentSecurityPolicy,omitempty"`
30+
Telemetry MultiKeyValue `yaml:"telemetry,omitempty"`
3231
}
3332

3433
type Proxy struct {

pkg/utils/utils.go

Lines changed: 29 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,12 @@ package utils
33
import (
44
"crypto/rand"
55
"encoding/base64"
6-
"encoding/json"
76
"fmt"
87
"strings"
98

109
"k8s.io/klog/v2"
1110

12-
consolev1 "github.com/openshift/api/console/v1"
11+
"github.com/openshift/console/pkg/serverconfig"
1312
)
1413

1514
const (
@@ -19,13 +18,16 @@ const (
1918
fontSrc = "font-src"
2019
scriptSrc = "script-src"
2120
styleSrc = "style-src"
21+
objectSrc = "object-src"
22+
connectSrc = "connect-src"
2223
consoleDot = "console.redhat.com"
2324
httpLocalHost = "http://localhost:8080"
2425
wsLocalHost = "ws://localhost:8080"
2526
self = "'self'"
2627
data = "data:"
2728
unsafeEval = "'unsafe-eval'"
2829
unsafeInline = "'unsafe-inline'"
30+
none = "'none'"
2931
)
3032

3133
// Generate a cryptographically secure random array of bytes.
@@ -54,7 +56,7 @@ func RandomString(length int) (string, error) {
5456
// buildCSPDirectives takes the content security policy configuration from the server and constructs
5557
// a complete set of directives for the Content-Security-Policy-Report-Only header.
5658
// The constructed directives will include the default sources and the supplied configuration.
57-
func BuildCSPDirectives(k8sMode, pluginsCSP, indexPageScriptNonce, cspReportingEndpoint string) ([]string, error) {
59+
func BuildCSPDirectives(k8sMode string, pluginsCSP serverconfig.MultiKeyValue, indexPageScriptNonce string, cspReportingEndpoint string) ([]string, error) {
5860
nonce := fmt.Sprintf("'nonce-%s'", indexPageScriptNonce)
5961

6062
// The default sources are the sources that are allowed for all directives.
@@ -68,38 +70,40 @@ func BuildCSPDirectives(k8sMode, pluginsCSP, indexPageScriptNonce, cspReportingE
6870
fontSrcDirective := []string{fontSrc, self}
6971
scriptSrcDirective := []string{scriptSrc, self, consoleDot}
7072
styleSrcDirective := []string{styleSrc, self}
73+
objectSrcDirective := []string{objectSrc, self}
74+
connectSrcDirective := []string{connectSrc, self, consoleDot}
75+
76+
// If running off-cluster, append the localhost sources to the default sources
7177
if k8sMode == "off-cluster" {
7278
baseUriDirective = append(baseUriDirective, []string{httpLocalHost, wsLocalHost}...)
7379
defaultSrcDirective = append(defaultSrcDirective, []string{httpLocalHost, wsLocalHost}...)
7480
imgSrcDirective = append(imgSrcDirective, httpLocalHost)
7581
fontSrcDirective = append(fontSrcDirective, httpLocalHost)
7682
scriptSrcDirective = append(scriptSrcDirective, []string{httpLocalHost, wsLocalHost}...)
7783
styleSrcDirective = append(styleSrcDirective, httpLocalHost)
84+
objectSrcDirective = append(objectSrcDirective, httpLocalHost)
85+
connectSrcDirective = append(connectSrcDirective, httpLocalHost)
7886
}
7987

8088
// If the plugins are providing a content security policy configuration, parse it and add it to
8189
// the appropriate directive. The configuration is a string that is parsed into a map of directive types to sources.
8290
// The sources are added to the existing sources for each type.
83-
if pluginsCSP != "" {
84-
parsedCSP, err := ParseContentSecurityPolicyConfig(pluginsCSP)
85-
if err != nil {
86-
return nil, err
87-
}
88-
for directive, sources := range *parsedCSP {
89-
switch directive {
90-
case consolev1.DefaultSrc:
91-
defaultSrcDirective = append(defaultSrcDirective, sources...)
92-
case consolev1.ImgSrc:
93-
imgSrcDirective = append(imgSrcDirective, sources...)
94-
case consolev1.FontSrc:
95-
fontSrcDirective = append(fontSrcDirective, sources...)
96-
case consolev1.ScriptSrc:
97-
scriptSrcDirective = append(scriptSrcDirective, sources...)
98-
case consolev1.StyleSrc:
99-
styleSrcDirective = append(styleSrcDirective, sources...)
100-
default:
101-
klog.Warningf("ignored invalid CSP directive: %v", directive)
102-
}
91+
for directive, sources := range pluginsCSP {
92+
switch directive {
93+
case defaultSrc:
94+
defaultSrcDirective = append(defaultSrcDirective, sources)
95+
case imgSrc:
96+
imgSrcDirective = append(imgSrcDirective, sources)
97+
case fontSrc:
98+
fontSrcDirective = append(fontSrcDirective, sources)
99+
case scriptSrc:
100+
scriptSrcDirective = append(scriptSrcDirective, sources)
101+
case styleSrc:
102+
styleSrcDirective = append(styleSrcDirective, sources)
103+
case connectSrc:
104+
connectSrcDirective = append(connectSrcDirective, sources)
105+
default:
106+
klog.Fatalf("invalid CSP directive: %s", directive)
103107
}
104108
}
105109

@@ -120,9 +124,10 @@ func BuildCSPDirectives(k8sMode, pluginsCSP, indexPageScriptNonce, cspReportingE
120124
strings.Join(fontSrcDirective, " "),
121125
strings.Join(scriptSrcDirective, " "),
122126
strings.Join(styleSrcDirective, " "),
127+
strings.Join(connectSrcDirective, " "),
128+
strings.Join(objectSrcDirective, " "),
123129
"frame-src 'none'",
124130
"frame-ancestors 'none'",
125-
"object-src 'none'",
126131
}
127132

128133
// Support using client provided CSP reporting endpoint for testing purposes.
@@ -132,41 +137,3 @@ func BuildCSPDirectives(k8sMode, pluginsCSP, indexPageScriptNonce, cspReportingE
132137

133138
return resultDirectives, nil
134139
}
135-
136-
func ParseContentSecurityPolicyConfig(csp string) (*map[consolev1.DirectiveType][]string, error) {
137-
parsedCSP := &map[consolev1.DirectiveType][]string{}
138-
err := json.Unmarshal([]byte(csp), parsedCSP)
139-
if err != nil {
140-
errMsg := fmt.Sprintf("Error unmarshaling ConsoleConfig contentSecurityPolicy field: %v", err)
141-
klog.Error(errMsg)
142-
return nil, fmt.Errorf(errMsg)
143-
}
144-
145-
// Validate the keys to ensure they are all valid DirectiveTypes
146-
for key := range *parsedCSP {
147-
// Check if the key is a valid DirectiveType
148-
if !isValidDirectiveType(key) {
149-
return nil, fmt.Errorf("invalid CSP directive: %v", key)
150-
}
151-
}
152-
153-
return parsedCSP, nil
154-
}
155-
156-
// Helper function to validate DirectiveTypes
157-
func isValidDirectiveType(d consolev1.DirectiveType) bool {
158-
validTypes := []consolev1.DirectiveType{
159-
consolev1.DefaultSrc,
160-
consolev1.ScriptSrc,
161-
consolev1.StyleSrc,
162-
consolev1.ImgSrc,
163-
consolev1.FontSrc,
164-
}
165-
166-
for _, validType := range validTypes {
167-
if d == validType {
168-
return true
169-
}
170-
}
171-
return false
172-
}

0 commit comments

Comments
 (0)