From d10d580ec38fa99ad0525426148a48041196a2aa Mon Sep 17 00:00:00 2001 From: Emma Turetsky Date: Wed, 5 Mar 2025 16:08:36 +0000 Subject: [PATCH] Added use of acceptable_authorization to origin scitokens files -- This will enforce origin capabilities for scitokens -- Adjusted the scitokens origin tests in authorization_test.go to test this functionality -- Added test files to xrootd/resources --- xrootd/authorization.go | 40 ++++-- xrootd/authorization_test.go | 115 ++++++++++++++---- xrootd/resources/scitokens.cfg | 3 + .../test-scitokens-origin-read-write.cfg | 34 ++++++ .../resources/test-scitokens-origin-read.cfg | 35 ++++++ .../resources/test-scitokens-origin-write.cfg | 35 ++++++ 6 files changed, 227 insertions(+), 35 deletions(-) create mode 100644 xrootd/resources/test-scitokens-origin-read-write.cfg create mode 100644 xrootd/resources/test-scitokens-origin-read.cfg create mode 100644 xrootd/resources/test-scitokens-origin-write.cfg diff --git a/xrootd/authorization.go b/xrootd/authorization.go index 37e71d09a..155c8e664 100644 --- a/xrootd/authorization.go +++ b/xrootd/authorization.go @@ -61,14 +61,15 @@ type ( // Per-issuer configuration Issuer struct { - Name string - Issuer string - BasePaths []string - RestrictedPaths []string - MapSubject bool - DefaultUser string - UsernameClaim string - NameMapfile string + Name string + Issuer string + BasePaths []string + RestrictedPaths []string + MapSubject bool + DefaultUser string + UsernameClaim string + NameMapfile string + AcceptableAuthorization string } // Top-level configuration object for the template @@ -479,6 +480,16 @@ func GenerateOriginIssuer(exportedPaths []string) (issuer Issuer, err error) { issuer.DefaultUser = param.Origin_ScitokensDefaultUser.GetString() issuer.UsernameClaim = param.Origin_ScitokensUsernameClaim.GetString() + // This will be set to true if PublicReads are enabled as well and is slightly redundant + // as then a token wouldn't be needed for reads, but doesn't actually change the functionality + // The default if the acceptable authorization isn't set is "all", so we only care if + // Reads is Enabled without Writes or vice versa + if param.Origin_EnableReads.GetBool() && !param.Origin_EnableWrites.GetBool() { + issuer.AcceptableAuthorization = "read" + } else if param.Origin_EnableWrites.GetBool() && !param.Origin_EnableReads.GetBool() { + issuer.AcceptableAuthorization = "write" + } + return } @@ -529,6 +540,17 @@ func makeSciTokensCfg() (cfg ScitokensCfg, err error) { return cfg, nil } +func resolveAuthorization(auth1 string, auth2 string) string { + if auth1 == auth2 { + return auth1 + } else { + // Either one is read and the other is write, in which case we want the default of + // all, or one is all and the other is read/write, and we'd still want it to be the default + // of all, which is equivalent to the empty string + return "" + } +} + // Writes out the server's scitokens.cfg configuration func EmitScitokensConfig(server server_structs.XRootDServer) error { if originServer, ok := server.(*origin.OriginServer); ok { @@ -577,6 +599,7 @@ func WriteOriginScitokensConfig(authedPaths []string) error { if val, ok := cfg.IssuerMap[issuer.Issuer]; ok { val.BasePaths = append(val.BasePaths, issuer.BasePaths...) val.Name += " and " + issuer.Name + val.AcceptableAuthorization = resolveAuthorization(val.AcceptableAuthorization, issuer.AcceptableAuthorization) cfg.IssuerMap[issuer.Issuer] = val } else { cfg.IssuerMap[issuer.Issuer] = issuer @@ -589,6 +612,7 @@ func WriteOriginScitokensConfig(authedPaths []string) error { if val, ok := cfg.IssuerMap[issuer.Issuer]; ok { val.BasePaths = append(val.BasePaths, issuer.BasePaths...) val.Name += " and " + issuer.Name + val.AcceptableAuthorization = resolveAuthorization(val.AcceptableAuthorization, issuer.AcceptableAuthorization) cfg.IssuerMap[issuer.Issuer] = val } else { cfg.IssuerMap[issuer.Issuer] = issuer diff --git a/xrootd/authorization_test.go b/xrootd/authorization_test.go index a5cc39e42..b1596d088 100644 --- a/xrootd/authorization_test.go +++ b/xrootd/authorization_test.go @@ -33,6 +33,7 @@ import ( "os" "path/filepath" "reflect" + "slices" "strings" "testing" @@ -77,6 +78,15 @@ var ( //go:embed resources/osdf-authfile authfileOutput string + //go:embed resources/test-scitokens-origin-read.cfg + scitokensOriginReadOutput string + + //go:embed resources/test-scitokens-origin-write.cfg + scitokensOriginWriteOutput string + + //go:embed resources/test-scitokens-origin-read-write.cfg + scitokensOriginReadWriteOutput string + sampleMultilineOutput = `foo \ bar baz @@ -556,6 +566,7 @@ func TestGenerateConfig(t *testing.T) { viper.Set("Origin.ScitokensMapSubject", true) viper.Set("Origin.Port", 8443) viper.Set("Server.WebPort", 8443) + viper.Set("Origin.EnableReads", true) config.InitConfigDir(viper.GetViper()) err = config.InitServer(ctx, server_structs.OriginType) require.NoError(t, err) @@ -568,6 +579,7 @@ func TestGenerateConfig(t *testing.T) { assert.Equal(t, "/another/exported/path", issuer.BasePaths[1]) assert.Equal(t, "user1", issuer.DefaultUser) assert.True(t, issuer.MapSubject) + assert.Equal(t, "read", issuer.AcceptableAuthorization) } func TestWriteOriginAuthFiles(t *testing.T) { @@ -727,37 +739,86 @@ func TestWriteCacheAuthFiles(t *testing.T) { } func TestWriteOriginScitokensConfig(t *testing.T) { - ctx, cancel, egrp := test_utils.TestContext(context.Background(), t) - defer func() { require.NoError(t, egrp.Wait()) }() - defer cancel() + tests := []struct { + Name string + Capabilities []string + ExpectedCfg string + InputCfg string + }{ + { + Name: "OriginSciTokensOnlyReads", + Capabilities: []string{"Reads"}, + ExpectedCfg: string(scitokensOriginReadOutput), + InputCfg: "", + }, + { + Name: "OriginSciTokensOnlyWrites", + Capabilities: []string{"Writes"}, + ExpectedCfg: string(scitokensOriginWriteOutput), + InputCfg: "", + }, + { + Name: "OriginSciTokensReadsWrites", + Capabilities: []string{"Reads", "Writes"}, + ExpectedCfg: string(scitokensOriginReadWriteOutput), + InputCfg: "", + }, + { + Name: "OriginScitokensMergeDiff", + Capabilities: []string{"Reads"}, + ExpectedCfg: string(monitoringOutput), + InputCfg: string(toMergeOutput), + }, + } + for _, test := range tests { + t.Run(test.Name, func(t *testing.T) { + ctx, cancel, egrp := test_utils.TestContext(context.Background(), t) + defer func() { require.NoError(t, egrp.Wait()) }() + defer cancel() - server_utils.ResetTestState() - dirname := t.TempDir() - os.Setenv("PELICAN_ORIGIN_RUNLOCATION", dirname) - defer os.Unsetenv("PELICAN_ORIGIN_RUNLOCATION") - config_dirname := t.TempDir() - viper.Set("Origin.SelfTest", true) - viper.Set("ConfigDir", config_dirname) - viper.Set("Origin.RunLocation", dirname) - viper.Set("Origin.Port", 8443) - viper.Set("Server.WebPort", 8444) - viper.Set("Server.Hostname", "origin.example.com") - viper.Set(param.Origin_StorageType.GetName(), string(server_structs.OriginStoragePosix)) + server_utils.ResetTestState() + defer server_utils.ResetTestState() - err := config.InitServer(ctx, server_structs.OriginType) - require.NoError(t, err) + viper.Set("Origin.Port", 8443) + viper.Set("Server.WebPort", 8444) + viper.Set("Server.Hostname", "origin.example.com") + viper.Set("Server.IssuerUrl", "https://origin.issuer.com") + viper.Set(param.Origin_StorageType.GetName(), string(server_structs.OriginStoragePosix)) - scitokensCfg := param.Xrootd_ScitokensConfig.GetString() - err = config.MkdirAll(filepath.Dir(scitokensCfg), 0755, -1, -1) - require.NoError(t, err) - err = os.WriteFile(scitokensCfg, []byte(toMergeOutput), 0640) - require.NoError(t, err) + dirname := t.TempDir() + os.Setenv("PELICAN_ORIGIN_RUNLOCATION", dirname) + defer os.Unsetenv("PELICAN_ORIGIN_RUNLOCATION") + config_dirname := t.TempDir() - err = WriteOriginScitokensConfig([]string{"/foo/bar"}) - require.NoError(t, err) + viper.Set("ConfigDir", config_dirname) + viper.Set("Origin.RunLocation", dirname) - genCfg, err := os.ReadFile(filepath.Join(dirname, "scitokens-origin-generated.cfg")) - require.NoError(t, err) + if slices.Contains(test.Capabilities, "Reads") { + viper.Set("Origin.EnableReads", true) + } + if slices.Contains(test.Capabilities, "Writes") { + viper.Set("Origin.EnableWrites", true) + } - assert.Equal(t, string(monitoringOutput), string(genCfg)) + err := config.InitServer(ctx, server_structs.OriginType) + require.NoError(t, err) + + if test.InputCfg != "" { + viper.Set("Server.IssuerUrl", "") + scitokensCfg := param.Xrootd_ScitokensConfig.GetString() + err = config.MkdirAll(filepath.Dir(scitokensCfg), 0755, -1, -1) + require.NoError(t, err) + err = os.WriteFile(scitokensCfg, []byte(test.InputCfg), 0640) + require.NoError(t, err) + } + + err = WriteOriginScitokensConfig([]string{"/foo/bar"}) + require.NoError(t, err) + + genCfg, err := os.ReadFile(filepath.Join(dirname, "scitokens-origin-generated.cfg")) + require.NoError(t, err) + + assert.Equal(t, test.ExpectedCfg, string(genCfg)) + }) + } } diff --git a/xrootd/resources/scitokens.cfg b/xrootd/resources/scitokens.cfg index 174e8438f..13b21ae17 100644 --- a/xrootd/resources/scitokens.cfg +++ b/xrootd/resources/scitokens.cfg @@ -41,6 +41,9 @@ name_mapfile = {{.NameMapfile}} {{- if .UsernameClaim}} username_claim = {{.UsernameClaim}} {{- end}} +{{- if .AcceptableAuthorization}} +acceptable_authorization = {{.AcceptableAuthorization}} +{{- end}} {{end -}} # End of config diff --git a/xrootd/resources/test-scitokens-origin-read-write.cfg b/xrootd/resources/test-scitokens-origin-read-write.cfg new file mode 100644 index 000000000..b36da7d43 --- /dev/null +++ b/xrootd/resources/test-scitokens-origin-read-write.cfg @@ -0,0 +1,34 @@ +# +# Copyright (C) 2024, Pelican Project, Morgridge Institute for Research +# +# Licensed under the Apache License, Version 2.0 (the "License"); you +# may not use this file except in compliance with the License. You may +# obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# +# This is a generated configuration file -- DO NOT HAND EDIT. +# It will be overwritten on the next startup of pelican. +# + +[Global] +audience_json = ["https://origin.example.com:8443"] + +[Issuer Built-in Monitoring] +issuer = https://origin.example.com:8444 +base_path = /pelican/monitoring +default_user = xrootd + +[Issuer Origin] +issuer = https://origin.issuer.com +base_path = /foo/bar + +# End of config diff --git a/xrootd/resources/test-scitokens-origin-read.cfg b/xrootd/resources/test-scitokens-origin-read.cfg new file mode 100644 index 000000000..bd0b47593 --- /dev/null +++ b/xrootd/resources/test-scitokens-origin-read.cfg @@ -0,0 +1,35 @@ +# +# Copyright (C) 2024, Pelican Project, Morgridge Institute for Research +# +# Licensed under the Apache License, Version 2.0 (the "License"); you +# may not use this file except in compliance with the License. You may +# obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# +# This is a generated configuration file -- DO NOT HAND EDIT. +# It will be overwritten on the next startup of pelican. +# + +[Global] +audience_json = ["https://origin.example.com:8443"] + +[Issuer Built-in Monitoring] +issuer = https://origin.example.com:8444 +base_path = /pelican/monitoring +default_user = xrootd + +[Issuer Origin] +issuer = https://origin.issuer.com +base_path = /foo/bar +acceptable_authorization = read + +# End of config diff --git a/xrootd/resources/test-scitokens-origin-write.cfg b/xrootd/resources/test-scitokens-origin-write.cfg new file mode 100644 index 000000000..fe9800a64 --- /dev/null +++ b/xrootd/resources/test-scitokens-origin-write.cfg @@ -0,0 +1,35 @@ +# +# Copyright (C) 2024, Pelican Project, Morgridge Institute for Research +# +# Licensed under the Apache License, Version 2.0 (the "License"); you +# may not use this file except in compliance with the License. You may +# obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# +# This is a generated configuration file -- DO NOT HAND EDIT. +# It will be overwritten on the next startup of pelican. +# + +[Global] +audience_json = ["https://origin.example.com:8443"] + +[Issuer Built-in Monitoring] +issuer = https://origin.example.com:8444 +base_path = /pelican/monitoring +default_user = xrootd + +[Issuer Origin] +issuer = https://origin.issuer.com +base_path = /foo/bar +acceptable_authorization = write + +# End of config