Prepare oidc token for playbook execution in executor.
The OIDC secrets are decrypted togheter with normal secrets, but the OIDC tokens are created right before the playbooks execution if needed for those playbooks. Specification: https://zuul-ci.org/docs/zuul/latest/developer/specs/zuul-workload-identity-federation.html Change-Id: I6dcc0e807da86fd07deae0505d313c8297f1c096
This commit is contained in:

committed by
James E. Blair

parent
73a31b3511
commit
52849f843c
@ -246,3 +246,9 @@ Version 34
|
||||
:Prior Zuul version: 11.3.0
|
||||
:Description: Don't store deprecated web ``status_url`` in system attributes anymore.
|
||||
Affects schedulers and web.
|
||||
|
||||
Version 35
|
||||
----------
|
||||
:Prior Zuul version: 11.3.0
|
||||
:Description: Updated Secret configuration foramat to support OIDC token.
|
||||
Affects schedulers and executors.
|
||||
|
@ -683,12 +683,15 @@ class FakeBuild(object):
|
||||
return False
|
||||
|
||||
def writeReturnData(self):
|
||||
changes = self.executor_server.return_data.get(self.name, {})
|
||||
data = changes.get(self.parameters['zuul']['ref'])
|
||||
if data is None:
|
||||
data_changes = self.executor_server.return_data.get(self.name, {})
|
||||
secret_data_changes = self.executor_server.return_secret_data.get(
|
||||
self.name, {})
|
||||
data = data_changes.get(self.parameters['zuul']['ref'])
|
||||
secret_data = secret_data_changes.get(self.parameters['zuul']['ref'])
|
||||
if data is None and secret_data is None:
|
||||
return
|
||||
with open(self.jobdir.result_data_file, 'w') as f:
|
||||
f.write(json.dumps({'data': data}))
|
||||
f.write(json.dumps({'data': data, 'secret_data': secret_data}))
|
||||
|
||||
def hasChanges(self, *changes):
|
||||
"""Return whether this build has certain changes in its git repos.
|
||||
@ -1033,6 +1036,7 @@ class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
|
||||
self.fail_tests = {}
|
||||
self.retry_tests = {}
|
||||
self.return_data = {}
|
||||
self.return_secret_data = {}
|
||||
self.job_builds = {}
|
||||
|
||||
def failJob(self, name, change):
|
||||
@ -1061,7 +1065,7 @@ class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
|
||||
dict(change=change,
|
||||
retries=retries))
|
||||
|
||||
def returnData(self, name, change, data):
|
||||
def returnData(self, name, change, data, secret_data={}):
|
||||
"""Instruct the executor to return data for this build.
|
||||
|
||||
:arg str name: The name of the job to return data.
|
||||
@ -1071,7 +1075,8 @@ class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
|
||||
:arg dict data: The data to return
|
||||
|
||||
"""
|
||||
changes = self.return_data.setdefault(name, {})
|
||||
data_changes = self.return_data.setdefault(name, {})
|
||||
secret_data_changes = self.return_secret_data.setdefault(name, {})
|
||||
if hasattr(change, 'number'):
|
||||
cid = change.data['currentPatchSet']['ref']
|
||||
elif isinstance(change, str):
|
||||
@ -1080,7 +1085,8 @@ class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
|
||||
# Not actually a change, but a ref update event for tags/etc
|
||||
# In this case a key of None is used by writeReturnData
|
||||
cid = None
|
||||
changes[cid] = data
|
||||
data_changes[cid] = data
|
||||
secret_data_changes[cid] = secret_data
|
||||
|
||||
def release(self, regex=None, change=None):
|
||||
"""Release a held build.
|
||||
|
2
tests/fixtures/config/multi-tenant/main.yaml
vendored
2
tests/fixtures/config/multi-tenant/main.yaml
vendored
@ -20,6 +20,8 @@
|
||||
- tenant:
|
||||
name: tenant-two
|
||||
max-nodes-per-job: 10
|
||||
allowed-oidc-issuers:
|
||||
- https://zuul.custom-issuer.com
|
||||
allowed-triggers: gerrit
|
||||
disallowed-labels:
|
||||
- tenant-one-.*
|
||||
|
2
tests/fixtures/config/secrets/git/org_project2/playbooks/parent.yaml
vendored
Normal file
2
tests/fixtures/config/secrets/git/org_project2/playbooks/parent.yaml
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
- hosts: all
|
||||
tasks: []
|
38
tests/fixtures/config/secrets/git/org_project2/zuul-oidc-iss-override.yaml
vendored
Normal file
38
tests/fixtures/config/secrets/git/org_project2/zuul-oidc-iss-override.yaml
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
- secret:
|
||||
name: oidc_secret
|
||||
oidc:
|
||||
ttl: 300
|
||||
algorithm: RS256
|
||||
claims:
|
||||
aud: sts.amazonaws.com
|
||||
my_claim: my_claim_value
|
||||
# Test it should not overwrite default claims
|
||||
sub: some_sub_value
|
||||
|
||||
- secret:
|
||||
name: oidc_secret_iss_override_allowed
|
||||
oidc:
|
||||
ttl: 300
|
||||
algorithm: RS256
|
||||
iss: https://zuul.allowed.com
|
||||
claims:
|
||||
aud: sts.amazonaws.com
|
||||
my_claim: my_claim_value
|
||||
# Test it should not overwrite default claims
|
||||
sub: some_sub_value
|
||||
|
||||
- job:
|
||||
parent: base
|
||||
name: project2-oidc-secret
|
||||
run: playbooks/secret.yaml
|
||||
secrets:
|
||||
- oidc_secret
|
||||
- oidc_secret_iss_override_allowed
|
||||
|
||||
- project:
|
||||
check:
|
||||
jobs:
|
||||
- project2-oidc-secret
|
||||
gate:
|
||||
jobs:
|
||||
- noop
|
65
tests/fixtures/config/secrets/git/org_project2/zuul-oidc-mix.yaml
vendored
Normal file
65
tests/fixtures/config/secrets/git/org_project2/zuul-oidc-mix.yaml
vendored
Normal file
@ -0,0 +1,65 @@
|
||||
- secret:
|
||||
name: project2_secret
|
||||
data:
|
||||
username: test-username
|
||||
password: !encrypted/pkcs1-oaep |
|
||||
BFhtdnm8uXx7kn79RFL/zJywmzLkT1GY78P3bOtp4WghUFWobkifSu7ZpaV4NeO0s71YUsi1wGZZ
|
||||
L0LveZjUN0t6OU1VZKSG8R5Ly7urjaSo1pPVIq5Rtt/H7W14Lecd+cUeKb4joeusC9drN3AA8a4o
|
||||
ykcVpt1wVqUnTbMGC9ARMCQP6eopcs1l7tzMseprW4RDNhIuz3CRgd0QBMPl6VDoFgBPB8vxtJw+
|
||||
3m0rqBYZCLZgCXekqlny8s2s92nJMuUABbJOEcDRarzibDsSXsfJt1y+5n7yOURsC7lovMg4GF/v
|
||||
Cl/0YMKjBO5bpv9EM5fToeKYyPGSKQoHOnCYceb3cAVcv5UawcCic8XjhEhp4K7WPdYf2HVAC/qt
|
||||
xhbpjTxG4U5Q/SoppOJ60WqEkQvbXs6n5Dvy7xmph6GWmU/bAv3eUK3pdD3xa2Ue1lHWz3U+rsYr
|
||||
aI+AKYsMYx3RBlfAmCeC1ve2BXPrqnOo7G8tnUvfdYPbK4Aakk0ds/AVqFHEZN+S6hRBmBjLaRFW
|
||||
Z3QSO1NjbBxWnaHKZYT7nkrJm8AMCgZU0ZArFLpaufKCeiK5ECSsDxic4FIsY1OkWT42qEUfL0Wd
|
||||
+150AKGNZpPJnnP3QYY4W/MWcKH/zdO400+zWN52WevbSqZy90tqKDJrBkMl1ydqbuw1E4ZHvIs=
|
||||
|
||||
- secret:
|
||||
name: project2_oidc_secret0
|
||||
oidc:
|
||||
ttl: 300
|
||||
algorithm: RS256
|
||||
claims:
|
||||
aud: sts.amazonaws.com
|
||||
my_claim: my_claim_value
|
||||
# Test it should not overwrite default claims
|
||||
sub: some_sub_value
|
||||
|
||||
- secret:
|
||||
name: project2_oidc_secret1
|
||||
oidc:
|
||||
ttl: 300
|
||||
algorithm: RS256
|
||||
claims:
|
||||
aud: sts.amazonaws.com
|
||||
my_claim: my_claim_value
|
||||
# Test it should not overwrite default claims
|
||||
sub: some_sub_value
|
||||
|
||||
- secret:
|
||||
name: project2_oidc_secret2
|
||||
oidc:
|
||||
ttl: 300
|
||||
algorithm: RS256
|
||||
claims:
|
||||
aud: sts.amazonaws.com
|
||||
my_claim: my_claim_value
|
||||
# Test it should not overwrite default claims
|
||||
sub: some_sub_value
|
||||
|
||||
- job:
|
||||
parent: base
|
||||
name: project2-oidc-secret
|
||||
run: playbooks/secret.yaml
|
||||
secrets:
|
||||
- project2_secret
|
||||
- project2_oidc_secret0
|
||||
- project2_oidc_secret1
|
||||
- project2_oidc_secret2
|
||||
|
||||
- project:
|
||||
check:
|
||||
jobs:
|
||||
- project2-oidc-secret
|
||||
gate:
|
||||
jobs:
|
||||
- noop
|
70
tests/fixtures/config/secrets/git/org_project2/zuul-oidc-multi.yaml
vendored
Normal file
70
tests/fixtures/config/secrets/git/org_project2/zuul-oidc-multi.yaml
vendored
Normal file
@ -0,0 +1,70 @@
|
||||
- secret:
|
||||
name: project2-oidc-secret0
|
||||
oidc:
|
||||
ttl: 300
|
||||
algorithm: RS256
|
||||
claims:
|
||||
aud: sts.amazonaws.com
|
||||
my_claim: my_claim_value
|
||||
# Test it should not overwrite default claims
|
||||
sub: some_sub_value
|
||||
|
||||
- secret:
|
||||
name: project2-oidc-secret1
|
||||
oidc:
|
||||
ttl: 300
|
||||
algorithm: RS256
|
||||
claims:
|
||||
aud: sts.amazonaws.com
|
||||
my_claim: my_claim_value
|
||||
# Test it should not overwrite default claims
|
||||
sub: some_sub_value
|
||||
|
||||
- secret:
|
||||
name: project2-oidc-secret2
|
||||
oidc:
|
||||
ttl: 300
|
||||
algorithm: RS256
|
||||
claims:
|
||||
aud: sts.amazonaws.com
|
||||
my_claim: my_claim_value
|
||||
# Test it should not overwrite default claims
|
||||
sub: some_sub_value
|
||||
|
||||
- job:
|
||||
parent: base
|
||||
name: project2-dependent-with-return
|
||||
run: playbooks/secret.yaml
|
||||
|
||||
- job:
|
||||
parent: base
|
||||
name: project2-parent
|
||||
pre-run: playbooks/parent.yaml
|
||||
post-run: playbooks/parent.yaml
|
||||
|
||||
- job:
|
||||
parent: project2-parent
|
||||
name: project2-oidc-secret
|
||||
run: playbooks/secret.yaml
|
||||
vars:
|
||||
login_secret0: login_secret0_value
|
||||
secrets:
|
||||
- secret: project2-oidc-secret0
|
||||
name: login_secret0 # Should override job var with same name
|
||||
- secret: project2-oidc-secret1
|
||||
name: login_secret1 # Should override zuul_return with same name
|
||||
pass-to-parent: true # And should be available in parent job
|
||||
- secret: project2-oidc-secret2
|
||||
name: login_secret2
|
||||
pass-to-parent: true # Should be available in parent job
|
||||
|
||||
- project:
|
||||
check:
|
||||
jobs:
|
||||
- project2-dependent-with-return
|
||||
- project2-oidc-secret:
|
||||
dependencies:
|
||||
- project2-dependent-with-return
|
||||
gate:
|
||||
jobs:
|
||||
- noop
|
25
tests/fixtures/config/secrets/git/org_project2/zuul-oidc-single.yaml
vendored
Normal file
25
tests/fixtures/config/secrets/git/org_project2/zuul-oidc-single.yaml
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
- secret:
|
||||
name: project2_oidc_secret
|
||||
oidc:
|
||||
ttl: 300
|
||||
algorithm: RS256
|
||||
claims:
|
||||
aud: sts.amazonaws.com
|
||||
my_claim: my_claim_value
|
||||
# Test it should not overwrite default claims
|
||||
sub: some_sub_value
|
||||
|
||||
- job:
|
||||
parent: base
|
||||
name: project2-oidc-secret
|
||||
run: playbooks/secret.yaml
|
||||
secrets:
|
||||
- project2_oidc_secret
|
||||
|
||||
- project:
|
||||
check:
|
||||
jobs:
|
||||
- project2-oidc-secret
|
||||
gate:
|
||||
jobs:
|
||||
- noop
|
2
tests/fixtures/config/secrets/main.yaml
vendored
2
tests/fixtures/config/secrets/main.yaml
vendored
@ -1,5 +1,7 @@
|
||||
- tenant:
|
||||
name: tenant-one
|
||||
allowed-oidc-issuers:
|
||||
- https://zuul.allowed.com
|
||||
source:
|
||||
gerrit:
|
||||
config-projects:
|
||||
|
@ -481,7 +481,11 @@ class TestJob(BaseTestCase):
|
||||
pre_idx = job.pre_run[0]['secrets']['mysecret']
|
||||
pre_secret = yaml.encrypted_load(
|
||||
job.secrets[pre_idx]['encrypted_data'])
|
||||
self.assertEqual(pre_secret, secret1_data)
|
||||
expected = {
|
||||
'secret_data': secret1_data,
|
||||
'secret_oidc': {},
|
||||
}
|
||||
self.assertEqual(expected, pre_secret)
|
||||
|
||||
# Verify that they were deduplicated
|
||||
pre2_idx = job.pre_run[0]['secrets']['othersecret']
|
||||
@ -491,7 +495,11 @@ class TestJob(BaseTestCase):
|
||||
run_idx = job.run[0]['secrets']['mysecret']
|
||||
run_secret = yaml.encrypted_load(
|
||||
job.secrets[run_idx]['encrypted_data'])
|
||||
self.assertEqual(run_secret, secret2_data)
|
||||
expected = {
|
||||
'secret_data': secret2_data,
|
||||
'secret_oidc': {},
|
||||
}
|
||||
self.assertEqual(expected, run_secret)
|
||||
|
||||
def _test_job_override_control(self, attr, job_attr,
|
||||
default, default_value,
|
||||
|
@ -13,6 +13,7 @@
|
||||
# under the License.
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
from zuul.zk.components import (
|
||||
COMPONENT_REGISTRY,
|
||||
@ -26,6 +27,7 @@ from tests.base import (
|
||||
simple_layout,
|
||||
iterate_timeout,
|
||||
model_version,
|
||||
FIXTURE_DIR,
|
||||
ZOOKEEPER_SESSION_TIMEOUT,
|
||||
)
|
||||
from zuul import model
|
||||
@ -489,3 +491,31 @@ class TestSemaphoreReleaseUpgrade(ZuulTestCase):
|
||||
dict(name='test-global-semaphore',
|
||||
result='SUCCESS', changes='2,1'),
|
||||
], ordered=False)
|
||||
|
||||
|
||||
class TestOidcSecretSupport(ZuulTestCase):
|
||||
tenant_config_file = 'config/secrets/main.yaml'
|
||||
|
||||
@model_version(34)
|
||||
def test_model_34(self):
|
||||
self._run_test()
|
||||
|
||||
@model_version(35)
|
||||
def test_model_35(self):
|
||||
self._run_test()
|
||||
|
||||
def _run_test(self):
|
||||
with open(os.path.join(FIXTURE_DIR,
|
||||
'config/secrets/git/',
|
||||
'org_project2/zuul-secret.yaml')) as f:
|
||||
config = f.read()
|
||||
file_dict = {'zuul.yaml': config}
|
||||
|
||||
A = self.fake_gerrit.addFakeChange('org/project2', 'master', 'A',
|
||||
files=file_dict)
|
||||
self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
|
||||
self.waitUntilSettled()
|
||||
self.assertEqual(A.reported, 1, "A should report success")
|
||||
self.assertHistory([
|
||||
dict(name='project2-secret', result='SUCCESS', changes='1,1'),
|
||||
])
|
||||
|
@ -13,9 +13,11 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import base64
|
||||
import copy
|
||||
import io
|
||||
import json
|
||||
import jwt
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
@ -7268,6 +7270,147 @@ class TestMaxTimeout(ZuulTestCase):
|
||||
"B should not fail because of timeout limit")
|
||||
|
||||
|
||||
class TestOIDCConfiguration(ZuulTestCase):
|
||||
tenant_config_file = 'config/multi-tenant/main.yaml'
|
||||
|
||||
def test_default_attributes(self):
|
||||
# Test that the secret oidc config with all default configurations
|
||||
# in different format.
|
||||
in_repo_conf = textwrap.dedent(
|
||||
"""
|
||||
- secret:
|
||||
name: my-oidc1
|
||||
oidc: {}
|
||||
- secret:
|
||||
name: my-oidc2
|
||||
oidc: null
|
||||
- secret:
|
||||
name: my-oidc3
|
||||
oidc:
|
||||
""")
|
||||
file_dict = {'.zuul.yaml': in_repo_conf}
|
||||
|
||||
A = self.fake_gerrit.addFakeChange('org/project2', 'master', 'A',
|
||||
files=file_dict)
|
||||
self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
|
||||
self.waitUntilSettled()
|
||||
self.assertIn("Build succeeded.", A.messages[0],
|
||||
"oidc config should allow null value")
|
||||
|
||||
def test_max_ttl_reached(self):
|
||||
# Test that the secret oidc ttl is within the tenant max-oidc-ttl
|
||||
in_repo_conf = textwrap.dedent(
|
||||
"""
|
||||
- secret:
|
||||
name: my-oidc
|
||||
oidc:
|
||||
ttl: 400
|
||||
""")
|
||||
file_dict = {'.zuul.yaml': in_repo_conf}
|
||||
# max-oidc-ttl for tenant-one is 300, it should cause error
|
||||
A = self.fake_gerrit.addFakeChange('org/project1', 'master', 'A',
|
||||
files=file_dict)
|
||||
self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
|
||||
self.waitUntilSettled()
|
||||
self.assertIn('The oidc secret "my-oidc" exceeds tenant max-oidc-ttl',
|
||||
A.messages[0], "A should fail because of ttl limit")
|
||||
# max-oidc-ttl for tenant-two is the default 10800,
|
||||
# which should not cause error
|
||||
B = self.fake_gerrit.addFakeChange('org/project2', 'master', 'A',
|
||||
files=file_dict)
|
||||
self.fake_gerrit.addEvent(B.getPatchsetCreatedEvent(1))
|
||||
self.waitUntilSettled()
|
||||
self.assertNotIn("exceeds tenant max-oidc-ttl", B.messages[0],
|
||||
"B should not fail because of ttl limit")
|
||||
|
||||
def test_custom_issuer(self):
|
||||
# Test that custom issuer is handled correctly
|
||||
in_repo_conf = textwrap.dedent(
|
||||
"""
|
||||
- secret:
|
||||
name: my-oidc
|
||||
oidc:
|
||||
ttl: 200
|
||||
iss: https://zuul.custom-issuer.com
|
||||
""")
|
||||
file_dict = {'.zuul.yaml': in_repo_conf}
|
||||
# The issuer is not white listed in tenant one, it should cause error
|
||||
A = self.fake_gerrit.addFakeChange('org/project1', 'master', 'A',
|
||||
files=file_dict)
|
||||
self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
|
||||
self.waitUntilSettled()
|
||||
self.assertIn(
|
||||
'The iss "https://zuul.custom-issuer.com" in'
|
||||
' oidc secret "my-oidc" is\n not allowed',
|
||||
A.messages[0], "A should fail because the issuer is not allowed"
|
||||
)
|
||||
# The issuer is white listed for tenant-two, no error
|
||||
B = self.fake_gerrit.addFakeChange('org/project2', 'master', 'A',
|
||||
files=file_dict)
|
||||
self.fake_gerrit.addEvent(B.getPatchsetCreatedEvent(1))
|
||||
self.waitUntilSettled()
|
||||
self.assertNotIn(
|
||||
'The iss "https://zuul.custom-issuer.com" in'
|
||||
' oidc secret "my-oidc" is\n not allowed',
|
||||
B.messages[0], "B should not fail because the issuer is allowed"
|
||||
)
|
||||
|
||||
def test_mutual_exclusive(self):
|
||||
# Test that `oidc` and `data` should be mutually exclusive
|
||||
in_repo_conf = textwrap.dedent(
|
||||
"""
|
||||
- secret:
|
||||
name: my-oidc
|
||||
oidc: {}
|
||||
data: {}
|
||||
""")
|
||||
file_dict = {'.zuul.yaml': in_repo_conf}
|
||||
A = self.fake_gerrit.addFakeChange('org/project1', 'master', 'A',
|
||||
files=file_dict)
|
||||
self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
|
||||
self.waitUntilSettled()
|
||||
self.assertIn('two or more values in the same group'
|
||||
' of exclusion \'secret_type\'',
|
||||
A.messages[0],
|
||||
"A should fail because of mutual exclusive")
|
||||
|
||||
def test_required(self):
|
||||
# Test that one of `oidc` and `data` must be present
|
||||
in_repo_conf = textwrap.dedent(
|
||||
"""
|
||||
- secret:
|
||||
name: my-oidc
|
||||
""")
|
||||
file_dict = {'.zuul.yaml': in_repo_conf}
|
||||
A = self.fake_gerrit.addFakeChange('org/project1', 'master', 'A',
|
||||
files=file_dict)
|
||||
self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
|
||||
self.waitUntilSettled()
|
||||
self.assertIn('Either \'data\' or \'oidc\' must be present',
|
||||
A.messages[0],
|
||||
"A should fail because both are missing")
|
||||
|
||||
def test_unsupported_algorithm(self):
|
||||
# Test that if the secret oidc algorithm is not supported,
|
||||
# there should be an error message
|
||||
in_repo_conf = textwrap.dedent(
|
||||
"""
|
||||
- secret:
|
||||
name: my-oidc
|
||||
oidc:
|
||||
algorithm: XX256
|
||||
""")
|
||||
file_dict = {'.zuul.yaml': in_repo_conf}
|
||||
|
||||
A = self.fake_gerrit.addFakeChange('org/project1', 'master', 'A',
|
||||
files=file_dict)
|
||||
self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
|
||||
self.waitUntilSettled()
|
||||
self.assertIn("Algorithm 'XX256' is not supported",
|
||||
A.messages[0],
|
||||
"A should fail because of unsupported algorithm")
|
||||
|
||||
|
||||
class TestAllowedConnection(AnsibleZuulTestCase):
|
||||
config_file = 'zuul-connections-gerrit-and-github.conf'
|
||||
tenant_config_file = 'config/multi-tenant/main.yaml'
|
||||
@ -7847,6 +7990,192 @@ class TestSecrets(ZuulTestCase):
|
||||
self.scheds.first.sched._runBlobStoreCleanup()
|
||||
self.assertEqual(len(bs), 0)
|
||||
|
||||
def test_oidc_single(self):
|
||||
# Test that oidc token is generated correctly when there is
|
||||
# a single oidc secret defined.
|
||||
with open(os.path.join(FIXTURE_DIR,
|
||||
'config/secrets/git/',
|
||||
'org_project2/zuul-oidc-single.yaml')) as f:
|
||||
config = f.read()
|
||||
file_dict = {'zuul.yaml': config}
|
||||
A = self.fake_gerrit.addFakeChange('org/project2', 'master', 'A',
|
||||
files=file_dict)
|
||||
self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
|
||||
self.waitUntilSettled()
|
||||
self.assertEqual(A.reported, 1, "A should report success")
|
||||
self.assertHistory([
|
||||
dict(name='project2-oidc-secret', result='SUCCESS', changes='1,1')
|
||||
])
|
||||
|
||||
secrets = self._getSecrets(
|
||||
'project2-oidc-secret', 'playbooks'
|
||||
)[0]
|
||||
self.assertEqual(len(secrets), 1)
|
||||
for secret_name, secret_content in secrets.items():
|
||||
self._validate_oidc_token(
|
||||
secret_name, secret_content.value, self.history[0].uuid)
|
||||
|
||||
def test_oidc_multi(self):
|
||||
# Test that oidc token is generated correctly when there are
|
||||
# multiple oidc secrets defined.
|
||||
with open(os.path.join(FIXTURE_DIR,
|
||||
'config/secrets/git/',
|
||||
'org_project2/zuul-oidc-multi.yaml')) as f:
|
||||
config = f.read()
|
||||
file_dict = {'zuul.yaml': config}
|
||||
A = self.fake_gerrit.addFakeChange('org/project2', 'master', 'A',
|
||||
files=file_dict)
|
||||
# Mock zuul_return data defined in playbook
|
||||
self.executor_server.returnData(
|
||||
"project2-dependent-with-return", A, data={'foo': 'bar'},
|
||||
secret_data={'login_secret1': "login_secret1_value"}
|
||||
)
|
||||
self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
|
||||
self.waitUntilSettled()
|
||||
self.assertEqual(A.reported, 1, "A should report success")
|
||||
self.assertHistory([
|
||||
dict(
|
||||
name='project2-dependent-with-return',
|
||||
result='SUCCESS', changes='1,1'
|
||||
),
|
||||
dict(
|
||||
name='project2-oidc-secret',
|
||||
result='SUCCESS', changes='1,1'
|
||||
)
|
||||
])
|
||||
|
||||
secrets = self._getSecrets(
|
||||
'project2-oidc-secret', 'playbooks'
|
||||
)[0]
|
||||
self.assertEqual(len(secrets), 3)
|
||||
|
||||
# OIDC secret should override job var with the same name
|
||||
self.assertNotEqual(secrets['login_secret0'], 'login_secret0_value')
|
||||
|
||||
# OIDC secret should override zuul_return secret with the same name
|
||||
self.assertNotEqual(secrets['login_secret1'], 'login_secret1_value')
|
||||
|
||||
# OIDC secret passed to parent
|
||||
parent_pre_secrets = self._getSecrets(
|
||||
'project2-oidc-secret', 'pre_playbooks'
|
||||
)[0]
|
||||
for secret_name, secret_content in parent_pre_secrets.items():
|
||||
self._validate_oidc_token(
|
||||
secret_name, secret_content.value,
|
||||
self.history[1].uuid, "parent.yaml")
|
||||
parent_post_secrets = self._getSecrets(
|
||||
'project2-oidc-secret', 'post_playbooks'
|
||||
)[0]
|
||||
for secret_name, secret_content in parent_post_secrets.items():
|
||||
self._validate_oidc_token(
|
||||
secret_name, secret_content.value,
|
||||
self.history[1].uuid, "parent.yaml")
|
||||
|
||||
for secret_name, secret_content in secrets.items():
|
||||
self._validate_oidc_token(
|
||||
secret_name, secret_content.value,
|
||||
self.history[1].uuid)
|
||||
|
||||
def test_oidc_mix(self):
|
||||
# Test that oidc token is generated correctly when there are
|
||||
# both oidc and normal secrets defined.
|
||||
with open(os.path.join(FIXTURE_DIR,
|
||||
'config/secrets/git/',
|
||||
'org_project2/zuul-oidc-mix.yaml')) as f:
|
||||
config = f.read()
|
||||
file_dict = {'zuul.yaml': config}
|
||||
A = self.fake_gerrit.addFakeChange('org/project2', 'master', 'A',
|
||||
files=file_dict)
|
||||
self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
|
||||
self.waitUntilSettled()
|
||||
self.assertEqual(A.reported, 1, "A should report success")
|
||||
self.assertHistory([
|
||||
dict(name='project2-oidc-secret', result='SUCCESS', changes='1,1')
|
||||
])
|
||||
|
||||
secrets = self._getSecrets(
|
||||
'project2-oidc-secret', 'playbooks'
|
||||
)[0]
|
||||
self.assertEqual(len(secrets), 4)
|
||||
|
||||
# Validate normal secrets
|
||||
self.assertEqual(
|
||||
secrets['project2_secret'],
|
||||
{'username': 'test-username', 'password': 'test-password'}
|
||||
)
|
||||
# Remove the normal secret from the dict and validate the oidc tokens
|
||||
del secrets['project2_secret']
|
||||
for secret_name, secret_content in secrets.items():
|
||||
self._validate_oidc_token(
|
||||
secret_name, secret_content.value, self.history[0].uuid)
|
||||
|
||||
def test_oidc_iss_override(self):
|
||||
# Test that the custom 'iss' is allowed when configured
|
||||
with open(os.path.join(
|
||||
FIXTURE_DIR,
|
||||
'config/secrets/git/',
|
||||
'org_project2/zuul-oidc-iss-override.yaml')) as f:
|
||||
config = f.read()
|
||||
file_dict = {'zuul.yaml': config}
|
||||
A = self.fake_gerrit.addFakeChange('org/project2', 'master', 'A',
|
||||
files=file_dict)
|
||||
self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
|
||||
self.waitUntilSettled()
|
||||
self.assertEqual(A.reported, 1, "A should report success")
|
||||
self.assertHistory([
|
||||
dict(name='project2-oidc-secret', result='SUCCESS', changes='1,1')
|
||||
])
|
||||
|
||||
secrets = self._getSecrets(
|
||||
'project2-oidc-secret', 'playbooks'
|
||||
)[0]
|
||||
self.assertEqual(len(secrets), 2)
|
||||
|
||||
expected_isses = {
|
||||
"oidc_secret": "https://zuul.example.com",
|
||||
"oidc_secret_iss_override_allowed": "https://zuul.allowed.com",
|
||||
}
|
||||
for secret_name, secret_content in secrets.items():
|
||||
self._validate_oidc_token(
|
||||
secret_name, secret_content.value, self.history[0].uuid,
|
||||
expected_iss=expected_isses[secret_name])
|
||||
|
||||
def _validate_oidc_token(self, oidc_name, oidc_token, build_uuid,
|
||||
playbook="secret.yaml",
|
||||
expected_iss="https://zuul.example.com"):
|
||||
|
||||
# Check header
|
||||
header_base64 = oidc_token.split('.')[0]
|
||||
header = json.loads(base64.b64decode(header_base64 + '==').decode())
|
||||
self.assertEqual(header['alg'], 'RS256')
|
||||
self.assertEqual(header['kid'], 'RS256-0')
|
||||
self.assertEqual(header['typ'], 'JWT')
|
||||
|
||||
# Decode and check payload
|
||||
keystore = self.scheds.first.sched.keystore
|
||||
_, public_key, _ = (keystore.getLatestOidcSigningKeys("RS256"))
|
||||
pem_public_key = encryption.serialize_rsa_public_key(public_key)
|
||||
payload = jwt.decode(
|
||||
oidc_token, pem_public_key, algorithms=["RS256"],
|
||||
audience="sts.amazonaws.com")
|
||||
self.assertEqual(payload['iss'], expected_iss)
|
||||
self.assertEqual(
|
||||
payload['sub'],
|
||||
'secret:tenant-one/review.example.com'
|
||||
f'/org/project2/{oidc_name}'
|
||||
)
|
||||
self.assertEqual(payload['build-uuid'], build_uuid)
|
||||
self.assertEqual(payload['job-name'], 'project2-oidc-secret')
|
||||
self.assertEqual(
|
||||
payload['playbook'],
|
||||
f'review.example.com/org/project2/playbooks/{playbook}'
|
||||
)
|
||||
self.assertEqual(payload['pipeline'], 'check')
|
||||
self.assertEqual(payload['tenant'], 'tenant-one')
|
||||
self.assertEqual(payload['aud'], 'sts.amazonaws.com')
|
||||
self.assertEqual(payload['my_claim'], 'my_claim_value')
|
||||
self.assertEqual(payload['exp'] - payload['iat'], 300)
|
||||
|
||||
|
||||
class TestSecretInheritance(ZuulTestCase):
|
||||
tenant_config_file = 'config/secret-inheritance/main.yaml'
|
||||
|
@ -42,6 +42,7 @@ from zuul.lib.varnames import check_varnames
|
||||
from zuul.zk.components import COMPONENT_REGISTRY
|
||||
from zuul.zk.semaphore import SemaphoreHandler
|
||||
from zuul.exceptions import (
|
||||
AlgorithmNotSupportedException,
|
||||
CleanupRunDeprecation,
|
||||
DuplicateGroupError,
|
||||
DuplicateNodeError,
|
||||
@ -638,12 +639,26 @@ class SecretParser(object):
|
||||
self.pcontext = pcontext
|
||||
self.schema = self.getSchema()
|
||||
|
||||
def _checkMissingAttribute(self, secret):
|
||||
if 'data' not in secret and 'oidc' not in secret:
|
||||
raise vs.Invalid("Either 'data' or 'oidc' must be present.")
|
||||
return secret
|
||||
|
||||
def getSchema(self):
|
||||
secret = {vs.Required('name'): str,
|
||||
vs.Required('data'): dict,
|
||||
'_source_context': model.SourceContext,
|
||||
'_start_mark': model.ZuulMark,
|
||||
}
|
||||
oidc_schema = vs.Any({
|
||||
'algorithm': str,
|
||||
'ttl': int,
|
||||
'iss': str,
|
||||
'claims': dict,
|
||||
}, None)
|
||||
|
||||
secret = vs.All({
|
||||
vs.Required('name'): str,
|
||||
vs.Exclusive('data', 'secret_type'): dict,
|
||||
vs.Exclusive('oidc', 'secret_type'): oidc_schema,
|
||||
'_source_context': model.SourceContext,
|
||||
'_start_mark': model.ZuulMark,
|
||||
}, self._checkMissingAttribute)
|
||||
|
||||
return vs.Schema(secret)
|
||||
|
||||
@ -652,7 +667,25 @@ class SecretParser(object):
|
||||
s = model.Secret(conf['name'], conf['_source_context'])
|
||||
s.source_context = conf['_source_context']
|
||||
s.start_mark = conf['_start_mark']
|
||||
s.secret_data = conf['data']
|
||||
if 'data' in conf:
|
||||
s.secret_data = conf['data']
|
||||
else:
|
||||
# TODO: A change to the global config would require
|
||||
# reloading the file where the secret is defined; this may
|
||||
# not be obvious to users, but it's also probably not
|
||||
# something we need to immediately solve. It probably
|
||||
# would be fixed by a full reconfiguration.
|
||||
glbl = self.pcontext.scheduler.globals
|
||||
if conf['oidc'] is None:
|
||||
conf['oidc'] = {}
|
||||
algorithm = conf['oidc'].get('algorithm')
|
||||
if not algorithm:
|
||||
conf['oidc']['algorithm'] = glbl.oidc_default_signing_algorithm
|
||||
elif algorithm not in glbl.oidc_supported_signing_algorithms:
|
||||
raise AlgorithmNotSupportedException(
|
||||
f"Algorithm '{algorithm}' is not supported")
|
||||
|
||||
s.secret_oidc = conf['oidc']
|
||||
return s
|
||||
|
||||
|
||||
@ -1878,6 +1911,7 @@ class TenantParser(object):
|
||||
'max-nodes-per-job': int,
|
||||
'max-job-timeout': int,
|
||||
'max-oidc-ttl': int,
|
||||
'allowed-oidc-issuers': to_list(str),
|
||||
'source': self.validateTenantSources(),
|
||||
'exclude-unprotected-branches': bool,
|
||||
'exclude-locked-branches': bool,
|
||||
@ -1923,6 +1957,10 @@ class TenantParser(object):
|
||||
tenant.max_job_timeout = int(conf['max-job-timeout'])
|
||||
if conf.get('max-oidc-ttl') is not None:
|
||||
tenant.max_oidc_ttl = int(conf['max-oidc-ttl'])
|
||||
if conf.get('default-oidc-ttl') is not None:
|
||||
tenant.default_oidc_ttl = int(conf['default-oidc-ttl'])
|
||||
if conf.get('allowed-oidc-issuers') is not None:
|
||||
tenant.allowed_oidc_issuers = as_list(conf['allowed-oidc-issuers'])
|
||||
if conf.get('exclude-unprotected-branches') is not None:
|
||||
tenant.exclude_unprotected_branches = \
|
||||
conf['exclude-unprotected-branches']
|
||||
|
@ -248,6 +248,28 @@ class MaxTimeoutError(ConfigurationSyntaxError):
|
||||
super(MaxTimeoutError, self).__init__(message)
|
||||
|
||||
|
||||
class MaxOIDCTTLError(ConfigurationSyntaxError):
|
||||
zuul_error_name = 'Max OIDC TTL Exceeded'
|
||||
|
||||
def __init__(self, secret, tenant):
|
||||
message = textwrap.dedent("""\
|
||||
The oidc secret "{secret}" exceeds tenant max-oidc-ttl {max_ttl}.""")
|
||||
message = textwrap.fill(message.format(
|
||||
secret=secret.name, max_ttl=tenant.max_oidc_ttl))
|
||||
super(MaxOIDCTTLError, self).__init__(message)
|
||||
|
||||
|
||||
class OIDCIssuerNotAllowedError(ConfigurationSyntaxError):
|
||||
zuul_error_name = 'OIDC issuer not allowed'
|
||||
|
||||
def __init__(self, secret, issuer):
|
||||
message = textwrap.dedent("""\
|
||||
The iss "{issuer}" in oidc secret "{secret}" is not allowed.""")
|
||||
message = textwrap.fill(message.format(
|
||||
secret=secret.name, issuer=issuer))
|
||||
super(OIDCIssuerNotAllowedError, self).__init__(message)
|
||||
|
||||
|
||||
class DuplicateGroupError(ConfigurationSyntaxError):
|
||||
zuul_error_name = 'Duplicate Nodeset Group'
|
||||
|
||||
|
@ -66,6 +66,11 @@ class ExecutorClient(object):
|
||||
uuid, self.sched.connections,
|
||||
job, item, pipeline, dependent_changes, merger_items,
|
||||
redact_secrets_and_keys=False)
|
||||
|
||||
# Pass webroot to the executor for generating oidc token
|
||||
web_root = manager.tenant.web_root
|
||||
if web_root:
|
||||
params["zuul_root_url"] = web_root.split("/t/")[0].rstrip("/")
|
||||
# TODO: deprecate and remove this variable?
|
||||
params["zuul"]["_inheritance_path"] = list(job.inheritance_path)
|
||||
|
||||
|
@ -18,6 +18,7 @@ import collections
|
||||
import copy
|
||||
import datetime
|
||||
import json
|
||||
import jwt
|
||||
import logging
|
||||
import multiprocessing
|
||||
import os
|
||||
@ -562,6 +563,7 @@ class JobDirPlaybook(object):
|
||||
self.secrets = os.path.join(self.secrets_root, 'all.yaml')
|
||||
self.secrets_content = None
|
||||
self.secrets_keys = set()
|
||||
self.secrets_oidc = {}
|
||||
self.semaphores = []
|
||||
self.nesting_level = None
|
||||
self.cleanup = False
|
||||
@ -2439,14 +2441,18 @@ class AnsibleJob(object):
|
||||
for role in playbook['roles']:
|
||||
self.prepareRole(jobdir_playbook, role, args)
|
||||
|
||||
secrets = self.decryptSecrets(playbook['secrets'])
|
||||
secrets = self.mergeSecretVars(secrets)
|
||||
secrets, jobdir_playbook.secrets_oidc = \
|
||||
self.decryptSecrets(playbook['secrets'])
|
||||
secrets = self.mergeSecretVars(secrets, jobdir_playbook.secrets_oidc)
|
||||
if secrets:
|
||||
check_varnames(secrets)
|
||||
secrets = yaml.mark_strings_unsafe(secrets)
|
||||
jobdir_playbook.secrets_content = yaml.ansible_unsafe_dump(
|
||||
secrets, default_flow_style=False)
|
||||
jobdir_playbook.secrets_keys = set(secrets.keys())
|
||||
if jobdir_playbook.secrets_oidc:
|
||||
jobdir_playbook.secrets_keys.update(
|
||||
jobdir_playbook.secrets_oidc.keys())
|
||||
|
||||
self.writeAnsibleConfig(jobdir_playbook)
|
||||
|
||||
@ -2462,10 +2468,11 @@ class AnsibleJob(object):
|
||||
:param dict secrets: The playbook secrets dictionary from the
|
||||
scheduler
|
||||
|
||||
:returns: A decrypted secrets dictionary
|
||||
:returns: Tuple of decrypted secrets dictionary and oidc dictionary
|
||||
|
||||
"""
|
||||
ret = {}
|
||||
ret_secret_data = {}
|
||||
ret_secret_oidc = {}
|
||||
with self.executor_server.zk_context as ctx:
|
||||
blobstore = BlobStore(ctx)
|
||||
for secret_name, secret_index in secrets.items():
|
||||
@ -2476,15 +2483,26 @@ class AnsibleJob(object):
|
||||
else:
|
||||
frozen_secret = self.job.secrets[secret_index]
|
||||
secret = zuul.model.Secret(secret_name, None)
|
||||
secret.secret_data = yaml.encrypted_load(
|
||||
decrypted_dict = yaml.encrypted_load(
|
||||
frozen_secret['encrypted_data'])
|
||||
private_secrets_key, public_secrets_key = \
|
||||
self.executor_server.keystore.getProjectSecretsKeys(
|
||||
frozen_secret['connection_name'],
|
||||
frozen_secret['project_name'])
|
||||
secret = secret.decrypt(private_secrets_key)
|
||||
ret[secret_name] = secret.secret_data
|
||||
return ret
|
||||
if "secret_oidc" not in decrypted_dict:
|
||||
# MODEL_API < 35
|
||||
secret.secret_data = decrypted_dict
|
||||
else:
|
||||
secret.secret_data = decrypted_dict['secret_data']
|
||||
secret.secret_oidc = decrypted_dict['secret_oidc']
|
||||
|
||||
if secret.secret_data:
|
||||
private_secrets_key, public_secrets_key = \
|
||||
self.executor_server.keystore.getProjectSecretsKeys(
|
||||
frozen_secret['connection_name'],
|
||||
frozen_secret['project_name'])
|
||||
secret = secret.decrypt(private_secrets_key)
|
||||
ret_secret_data[secret_name] = secret.secret_data
|
||||
else:
|
||||
ret_secret_oidc[secret_name] = secret.secret_oidc
|
||||
|
||||
return ret_secret_data, ret_secret_oidc
|
||||
|
||||
def checkoutTrustedProject(self, project, branch, args):
|
||||
pi = self.jobdir.getTrustedProject(project.canonical_name,
|
||||
@ -2606,11 +2624,12 @@ class AnsibleJob(object):
|
||||
project.name)
|
||||
return path
|
||||
|
||||
def mergeSecretVars(self, secrets):
|
||||
def mergeSecretVars(self, secrets, secrets_oidc):
|
||||
'''
|
||||
Merge secret return data with secrets.
|
||||
|
||||
:arg secrets dict: Actual Zuul secrets.
|
||||
:arg secrets_oidc dict: Actual Zuul oidc secrets.
|
||||
'''
|
||||
|
||||
secret_vars = self.secret_vars
|
||||
@ -2629,6 +2648,7 @@ class AnsibleJob(object):
|
||||
other_vars.update(host_vars.keys())
|
||||
other_vars.update(self.job.extra_variables.keys())
|
||||
other_vars.update(secrets.keys())
|
||||
other_vars.update(secrets_oidc.keys())
|
||||
|
||||
ret = secret_vars.copy()
|
||||
for key in other_vars:
|
||||
@ -3585,9 +3605,66 @@ class AnsibleJob(object):
|
||||
now=datetime.datetime.now(),
|
||||
msg=msg))
|
||||
|
||||
def _generateOidcTokens(self, playbook):
|
||||
# In case web_root is not configured in zuul and tenant,
|
||||
# 'zuul_root_url' would not be available in the arguments,
|
||||
# just log a warning and skip generating oidc tokens
|
||||
if 'zuul_root_url' not in self.arguments:
|
||||
self.log.warning(
|
||||
"web_root is not configured in zuul, "
|
||||
"skipping oidc token generation")
|
||||
return
|
||||
oidc_tokens = {}
|
||||
for oidc_name, oidc_config in playbook.secrets_oidc.items():
|
||||
algorithm = oidc_config['algorithm']
|
||||
private_secrets_key, _, version = \
|
||||
self.executor_server.keystore.getLatestOidcSigningKeys(
|
||||
algorithm=algorithm)
|
||||
iat = int(time.time())
|
||||
ttl = oidc_config['ttl']
|
||||
exp = iat + ttl
|
||||
tenant = self.arguments['zuul']['tenant']
|
||||
canonical_project_name = \
|
||||
self.arguments['zuul']['project']['canonical_name']
|
||||
sub = f'secret:{tenant}/{canonical_project_name}/{oidc_name}'
|
||||
iss = oidc_config.get('iss', self.arguments["zuul_root_url"])
|
||||
|
||||
payload = {
|
||||
"iss": iss,
|
||||
'sub': sub,
|
||||
'build-uuid': self.arguments["zuul"]["build"],
|
||||
'job-name': self.arguments['zuul']['job'],
|
||||
'playbook': playbook.canonical_name_and_path,
|
||||
'pipeline': self.arguments['zuul']['pipeline'],
|
||||
'tenant': tenant,
|
||||
'iat': iat,
|
||||
'exp': exp,
|
||||
}
|
||||
custom_claims = oidc_config.get('claims', {})
|
||||
for key, value in custom_claims.items():
|
||||
# custom claims should not overwrite the default claims
|
||||
if key not in payload:
|
||||
payload[key] = value
|
||||
|
||||
token = jwt.encode(
|
||||
payload, private_secrets_key, algorithm=algorithm,
|
||||
headers={"kid": f"{algorithm}-{version}"})
|
||||
oidc_tokens[oidc_name] = token
|
||||
|
||||
oidc_tokens_content = yaml.ansible_unsafe_dump(
|
||||
yaml.mark_strings_unsafe(oidc_tokens), default_flow_style=False)
|
||||
|
||||
if playbook.secrets_content:
|
||||
playbook.secrets_content += oidc_tokens_content
|
||||
else:
|
||||
playbook.secrets_content = oidc_tokens_content
|
||||
|
||||
def runAnsiblePlaybook(self, playbook, timeout, ansible_version,
|
||||
success=None, phase=None, index=None,
|
||||
will_retry=None):
|
||||
if playbook.secrets_oidc:
|
||||
self._generateOidcTokens(playbook)
|
||||
|
||||
if playbook.trusted or playbook.secrets_content:
|
||||
self.writeInventory(playbook, self.frozen_hostvars)
|
||||
else:
|
||||
|
@ -40,7 +40,9 @@ from zuul import change_matcher
|
||||
from zuul.exceptions import (
|
||||
SEVERITY_ERROR,
|
||||
SEVERITY_WARNING,
|
||||
OIDCIssuerNotAllowedError,
|
||||
LabelForbiddenError,
|
||||
MaxOIDCTTLError,
|
||||
MaxTimeoutError,
|
||||
NodesetNotFoundError,
|
||||
ProjectNotFoundError,
|
||||
@ -2761,6 +2763,9 @@ class Secret(ConfigObject):
|
||||
# is named 'secret_data' to make it easy to search for and
|
||||
# spot where it is directly used.
|
||||
self.secret_data = {}
|
||||
# This attribute stores the oidc token configuration for the
|
||||
# oidc secrets. Mutually exclusive with secret_data.
|
||||
self.secret_oidc = {}
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
@ -2769,7 +2774,8 @@ class Secret(ConfigObject):
|
||||
if not isinstance(other, Secret):
|
||||
return False
|
||||
return (self.name == other.name and
|
||||
self.secret_data == other.secret_data)
|
||||
self.secret_data == other.secret_data and
|
||||
self.secret_oidc == other.secret_oidc)
|
||||
|
||||
def __repr__(self):
|
||||
return '<Secret %s>' % (self.name,)
|
||||
@ -2801,8 +2807,26 @@ class Secret(ConfigObject):
|
||||
r.secret_data = self._decrypt(private_key, self.secret_data)
|
||||
return r
|
||||
|
||||
def serialize(self):
|
||||
return yaml.encrypted_dump(self.secret_data, default_flow_style=False)
|
||||
def serialize(self, layout):
|
||||
# The output of this method is used by the executor
|
||||
# Set the ttl for this tenant
|
||||
if self.secret_oidc:
|
||||
secret_oidc = self.secret_oidc.copy()
|
||||
# We previously checked that it's not greater than the tenant
|
||||
# max.
|
||||
secret_oidc['ttl'] = secret_oidc.get(
|
||||
'ttl', layout.tenant.default_oidc_ttl)
|
||||
else:
|
||||
secret_oidc = {}
|
||||
|
||||
if COMPONENT_REGISTRY.model_api >= 35:
|
||||
data = {
|
||||
"secret_data": self.secret_data,
|
||||
"secret_oidc": secret_oidc
|
||||
}
|
||||
else:
|
||||
data = self.secret_data
|
||||
return yaml.encrypted_dump(data, default_flow_style=False)
|
||||
|
||||
|
||||
class SecretUse(ConfigObject):
|
||||
@ -3025,7 +3049,7 @@ class PlaybookContext(ConfigObject):
|
||||
for secret_use in self.secrets:
|
||||
secret = layout.secrets.get(secret_use.name)
|
||||
secret_name = secret_use.alias
|
||||
encrypted_secret_data = secret.serialize()
|
||||
encrypted_secret_data = secret.serialize(layout)
|
||||
# Use *our* project, not the secret's, because we want to decrypt
|
||||
# with *our* key.
|
||||
project = layout.tenant.getProject(
|
||||
@ -4573,7 +4597,7 @@ class Job(ConfigObject):
|
||||
raise SecretNotFoundError(
|
||||
"Secret %s not found" % (secret_use.name,))
|
||||
secret_name = secret_use.alias
|
||||
encrypted_secret_data = secret.serialize()
|
||||
encrypted_secret_data = secret.serialize(layout)
|
||||
# Use the other project, not the secret's, because we
|
||||
# want to decrypt with the other project's key key.
|
||||
connection_name = other.source_context.project_connection_name
|
||||
@ -9604,7 +9628,18 @@ class Layout(object):
|
||||
self._checkAddNodeset(nodeset)
|
||||
self._addIdenticalObject('Nodeset', self.nodesets, nodeset)
|
||||
|
||||
def _checkAddSecret(self, secret):
|
||||
if secret.secret_oidc:
|
||||
ttl = secret.secret_oidc.get('ttl', self.tenant.default_oidc_ttl)
|
||||
if int(ttl) > self.tenant.max_oidc_ttl:
|
||||
raise MaxOIDCTTLError(secret, self.tenant)
|
||||
|
||||
iss = secret.secret_oidc.get('iss')
|
||||
if iss and iss not in self.tenant.allowed_oidc_issuers:
|
||||
raise OIDCIssuerNotAllowedError(secret, iss)
|
||||
|
||||
def addSecret(self, secret):
|
||||
self._checkAddSecret(secret)
|
||||
self._addIdenticalObject('Secret', self.secrets, secret)
|
||||
|
||||
def addSemaphore(self, semaphore):
|
||||
@ -10169,6 +10204,8 @@ class Tenant(object):
|
||||
self.max_nodes_per_job = 5
|
||||
self.max_job_timeout = 10800
|
||||
self.max_oidc_ttl = 10800
|
||||
self.default_oidc_ttl = 3600
|
||||
self.allowed_oidc_issuers = []
|
||||
self.max_changes_per_pipeline = None
|
||||
self.max_dependencies = None
|
||||
self.exclude_unprotected_branches = False
|
||||
|
@ -14,4 +14,4 @@
|
||||
|
||||
# When making ZK schema changes, increment this and add a record to
|
||||
# doc/source/developer/model-changelog.rst
|
||||
MODEL_API = 34
|
||||
MODEL_API = 35
|
||||
|
Reference in New Issue
Block a user