Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 22 additions & 21 deletions services/proxy/pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,27 +24,28 @@ type Config struct {
GRPCClientTLS *shared.GRPCClientTLS `yaml:"grpc_client_tls"`
GrpcClient client.Client `yaml:"-"`

RoleQuotas map[string]uint64 `yaml:"role_quotas"`
Policies []Policy `yaml:"policies"`
AdditionalPolicies []Policy `yaml:"additional_policies"`
OIDC OIDC `yaml:"oidc"`
ServiceAccount ServiceAccount `yaml:"service_account"`
RoleAssignment RoleAssignment `yaml:"role_assignment"`
PolicySelector *PolicySelector `yaml:"policy_selector"`
PreSignedURL PreSignedURL `yaml:"pre_signed_url"`
AccountBackend string `yaml:"account_backend" env:"PROXY_ACCOUNT_BACKEND_TYPE" desc:"Account backend the PROXY service should use. Currently only 'cs3' is possible here." introductionVersion:"1.0.0"`
UserOIDCClaim string `yaml:"user_oidc_claim" env:"PROXY_USER_OIDC_CLAIM" desc:"The name of an OpenID Connect claim that is used for resolving users with the account backend. The value of the claim must hold a per user unique, stable and non re-assignable identifier. The availability of claims depends on your Identity Provider. There are common claims available for most Identity providers like 'email' or 'preferred_username' but you can also add your own claim." introductionVersion:"1.0.0"`
UserCS3Claim string `yaml:"user_cs3_claim" env:"PROXY_USER_CS3_CLAIM" desc:"The name of a CS3 user attribute (claim) that should be mapped to the 'user_oidc_claim'. Supported values are 'username', 'mail' and 'userid'." introductionVersion:"1.0.0"`
MachineAuthAPIKey string `yaml:"machine_auth_api_key" env:"OC_MACHINE_AUTH_API_KEY;PROXY_MACHINE_AUTH_API_KEY" desc:"Machine auth API key used to validate internal requests necessary to access resources from other services." introductionVersion:"1.0.0" mask:"password"`
AutoprovisionAccounts bool `yaml:"auto_provision_accounts" env:"PROXY_AUTOPROVISION_ACCOUNTS" desc:"Set this to 'true' to automatically provision users that do not yet exist in the users service on-demand upon first sign-in. To use this a write-enabled libregraph user backend needs to be setup an running." introductionVersion:"1.0.0"`
AutoProvisionClaims AutoProvisionClaims `yaml:"auto_provision_claims"`
EnableBasicAuth bool `yaml:"enable_basic_auth" env:"PROXY_ENABLE_BASIC_AUTH" desc:"Set this to true to enable 'basic authentication' (username/password)." introductionVersion:"1.0.0"`
InsecureBackends bool `yaml:"insecure_backends" env:"PROXY_INSECURE_BACKENDS" desc:"Disable TLS certificate validation for all HTTP backend connections." introductionVersion:"1.0.0"`
BackendHTTPSCACert string `yaml:"backend_https_cacert" env:"PROXY_HTTPS_CACERT" desc:"Path/File for the root CA certificate used to validate the server’s TLS certificate for https enabled backend services." introductionVersion:"1.0.0"`
AuthMiddleware AuthMiddleware `yaml:"auth_middleware"`
PoliciesMiddleware PoliciesMiddleware `yaml:"policies_middleware"`
CSPConfigFileLocation string `yaml:"csp_config_file_location" env:"PROXY_CSP_CONFIG_FILE_LOCATION" desc:"The location of the CSP configuration file." introductionVersion:"1.0.0"`
Events Events `yaml:"events"`
RoleQuotas map[string]uint64 `yaml:"role_quotas"`
Policies []Policy `yaml:"policies"`
AdditionalPolicies []Policy `yaml:"additional_policies"`
OIDC OIDC `yaml:"oidc"`
ServiceAccount ServiceAccount `yaml:"service_account"`
RoleAssignment RoleAssignment `yaml:"role_assignment"`
PolicySelector *PolicySelector `yaml:"policy_selector"`
PreSignedURL PreSignedURL `yaml:"pre_signed_url"`
AccountBackend string `yaml:"account_backend" env:"PROXY_ACCOUNT_BACKEND_TYPE" desc:"Account backend the PROXY service should use. Currently only 'cs3' is possible here." introductionVersion:"1.0.0"`
UserOIDCClaim string `yaml:"user_oidc_claim" env:"PROXY_USER_OIDC_CLAIM" desc:"The name of an OpenID Connect claim that is used for resolving users with the account backend. The value of the claim must hold a per user unique, stable and non re-assignable identifier. The availability of claims depends on your Identity Provider. There are common claims available for most Identity providers like 'email' or 'preferred_username' but you can also add your own claim." introductionVersion:"1.0.0"`
UserCS3Claim string `yaml:"user_cs3_claim" env:"PROXY_USER_CS3_CLAIM" desc:"The name of a CS3 user attribute (claim) that should be mapped to the 'user_oidc_claim'. Supported values are 'username', 'mail' and 'userid'." introductionVersion:"1.0.0"`
MachineAuthAPIKey string `yaml:"machine_auth_api_key" env:"OC_MACHINE_AUTH_API_KEY;PROXY_MACHINE_AUTH_API_KEY" desc:"Machine auth API key used to validate internal requests necessary to access resources from other services." introductionVersion:"1.0.0" mask:"password"`
AutoprovisionAccounts bool `yaml:"auto_provision_accounts" env:"PROXY_AUTOPROVISION_ACCOUNTS" desc:"Set this to 'true' to automatically provision users that do not yet exist in the users service on-demand upon first sign-in. To use this a write-enabled libregraph user backend needs to be setup an running." introductionVersion:"1.0.0"`
AutoProvisionClaims AutoProvisionClaims `yaml:"auto_provision_claims"`
EnableBasicAuth bool `yaml:"enable_basic_auth" env:"PROXY_ENABLE_BASIC_AUTH" desc:"Set this to true to enable 'basic authentication' (username/password)." introductionVersion:"1.0.0"`
InsecureBackends bool `yaml:"insecure_backends" env:"PROXY_INSECURE_BACKENDS" desc:"Disable TLS certificate validation for all HTTP backend connections." introductionVersion:"1.0.0"`
BackendHTTPSCACert string `yaml:"backend_https_cacert" env:"PROXY_HTTPS_CACERT" desc:"Path/File for the root CA certificate used to validate the server’s TLS certificate for https enabled backend services." introductionVersion:"1.0.0"`
AuthMiddleware AuthMiddleware `yaml:"auth_middleware"`
PoliciesMiddleware PoliciesMiddleware `yaml:"policies_middleware"`
CSPConfigFileLocation string `yaml:"csp_config_file_location" env:"PROXY_CSP_CONFIG_FILE_LOCATION" desc:"The location of the CSP configuration file." introductionVersion:"1.0.0"`
CSPConfigFileOverrideLocation string `yaml:"csp_config_file_override_location" env:"PROXY_CSP_CONFIG_FILE_OVERRIDE_LOCATION" desc:"The location of the CSP configuration file override." introductionVersion:"%%NEXT%%"`
Events Events `yaml:"events"`

Context context.Context `json:"-" yaml:"-"`
}
Expand Down
7 changes: 4 additions & 3 deletions services/proxy/pkg/config/defaults/defaultconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,10 @@ func DefaultConfig() *config.Config {
DisplayName: "name",
Groups: "groups",
},
EnableBasicAuth: false,
InsecureBackends: false,
CSPConfigFileLocation: "",
EnableBasicAuth: false,
InsecureBackends: false,
CSPConfigFileLocation: "",
CSPConfigFileOverrideLocation: "",
Events: config.Events{
Endpoint: "127.0.0.1:9233",
Cluster: "opencloud-cluster",
Expand Down
99 changes: 92 additions & 7 deletions services/proxy/pkg/middleware/security.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,48 @@ package middleware
import (
"net/http"
"os"
"reflect"

gofig "github.com/gookit/config/v2"
"github.com/gookit/config/v2/yaml"
"github.com/opencloud-eu/opencloud/services/proxy/pkg/config"
"github.com/unrolled/secure"
"github.com/unrolled/secure/cspbuilder"
yamlv3 "gopkg.in/yaml.v3"
)

// LoadCSPConfig loads CSP header configuration from a yaml file.
func LoadCSPConfig(proxyCfg *config.Config) (*config.CSP, error) {
yamlContent, err := loadCSPYaml(proxyCfg)
yamlContent, customYamlContent, err := loadCSPYaml(proxyCfg)
if err != nil {
return nil, err
}
return loadCSPConfig(yamlContent)
return loadCSPConfig(yamlContent, customYamlContent)
}

// LoadCSPConfig loads CSP header configuration from a yaml file.
func loadCSPConfig(yamlContent []byte) (*config.CSP, error) {
func loadCSPConfig(presetYamlContent, customYamlContent []byte) (*config.CSP, error) {
// substitute env vars and load to struct
gofig.WithOptions(gofig.ParseEnv)
gofig.AddDriver(yaml.Driver)

err := gofig.LoadSources("yaml", yamlContent)
presetMap := map[string]interface{}{}
err := yamlv3.Unmarshal(presetYamlContent, &presetMap)
if err != nil {
return nil, err
}
customMap := map[string]interface{}{}
err = yamlv3.Unmarshal(customYamlContent, &customMap)
if err != nil {
return nil, err
}
mergedMap := deepMerge(presetMap, customMap)
mergedYamlContent, err := yamlv3.Marshal(mergedMap)
if err != nil {
return nil, err
}

err = gofig.LoadSources("yaml", mergedYamlContent)
if err != nil {
return nil, err
}
Expand All @@ -41,11 +59,78 @@ func loadCSPConfig(yamlContent []byte) (*config.CSP, error) {
return &cspConfig, nil
}

func loadCSPYaml(proxyCfg *config.Config) ([]byte, error) {
// deepMerge recursively merges map2 into map1.
// - nested maps are merged recursively
// - slices are concatenated, preserving order and avoiding duplicates
// - scalar or type-mismatched values from map2 overwrite map1
func deepMerge(map1, map2 map[string]interface{}) map[string]interface{} {
if map1 == nil {
out := make(map[string]interface{}, len(map2))
for k, v := range map2 {
out[k] = v
}
return out
}

for k, v2 := range map2 {
if v1, ok := map1[k]; ok {
// both maps -> recurse
if m1, ok1 := v1.(map[string]interface{}); ok1 {
if m2, ok2 := v2.(map[string]interface{}); ok2 {
map1[k] = deepMerge(m1, m2)
continue
}
}

// both slices -> merge unique
if s1, ok1 := v1.([]interface{}); ok1 {
if s2, ok2 := v2.([]interface{}); ok2 {
merged := append([]interface{}{}, s1...)
for _, item := range s2 {
if !sliceContains(merged, item) {
merged = append(merged, item)
}
}
map1[k] = merged
continue
}
// s1 is slice, v2 single -> append if missing
if !sliceContains(s1, v2) {
map1[k] = append(s1, v2)
}
continue
}

// default: overwrite
map1[k] = v2
} else {
// new key -> just set
map1[k] = v2
}
}

return map1
}

func sliceContains(slice []interface{}, val interface{}) bool {
for _, v := range slice {
if reflect.DeepEqual(v, val) {
return true
}
}
return false
}

func loadCSPYaml(proxyCfg *config.Config) ([]byte, []byte, error) {
if proxyCfg.CSPConfigFileOverrideLocation != "" {
overrideCSPYaml, err := os.ReadFile(proxyCfg.CSPConfigFileOverrideLocation)
return overrideCSPYaml, []byte{}, err
}
if proxyCfg.CSPConfigFileLocation == "" {
return []byte(config.DefaultCSPConfig), nil
return []byte(config.DefaultCSPConfig), nil, nil
}
return os.ReadFile(proxyCfg.CSPConfigFileLocation)
customCSPYaml, err := os.ReadFile(proxyCfg.CSPConfigFileLocation)
return []byte(config.DefaultCSPConfig), customCSPYaml, err
}

// Security is a middleware to apply security relevant http headers like CSP.
Expand Down
24 changes: 18 additions & 6 deletions services/proxy/pkg/middleware/security_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import (
"testing"

"gotest.tools/v3/assert"
"gotest.tools/v3/assert/cmp"
)

func TestLoadCSPConfig(t *testing.T) {
// setup test env
yaml := `
presetYaml := `
directives:
frame-src:
- '''self'''
Expand All @@ -17,12 +18,23 @@ directives:
- 'https://${COLLABORA_DOMAIN|collabora.opencloud.test}/'
`

config, err := loadCSPConfig([]byte(yaml))
customYaml := `
directives:
img-src:
- '''self'''
- 'data:'
frame-src:
- 'https://some.custom.domain/'
`
config, err := loadCSPConfig([]byte(presetYaml), []byte(customYaml))
if err != nil {
t.Error(err)
}
assert.Equal(t, config.Directives["frame-src"][0], "'self'")
assert.Equal(t, config.Directives["frame-src"][1], "https://embed.diagrams.net/")
assert.Equal(t, config.Directives["frame-src"][2], "https://onlyoffice.opencloud.test/")
assert.Equal(t, config.Directives["frame-src"][3], "https://collabora.opencloud.test/")
assert.Assert(t, cmp.Contains(config.Directives["frame-src"], "'self'"))
assert.Assert(t, cmp.Contains(config.Directives["frame-src"], "https://embed.diagrams.net/"))
assert.Assert(t, cmp.Contains(config.Directives["frame-src"], "https://onlyoffice.opencloud.test/"))
assert.Assert(t, cmp.Contains(config.Directives["frame-src"], "https://collabora.opencloud.test/"))

assert.Assert(t, cmp.Contains(config.Directives["img-src"], "'self'"))
assert.Assert(t, cmp.Contains(config.Directives["img-src"], "data:"))
}