Remove token bind capabilities
Token bind operations were deprecated in Pike with UUID tokens and staged for removal in Rocky. https://review.openstack.org/#/c/428388/ This change does keep a configuration option around since it was not officially deprecated with the rest of the token bind functionality. The option is being officially deprecated in this commit and additional context about the change was added to the help text for the option. bp removed-as-of-rocky Change-Id: I7a42408893c782bcc20fb40ebba5f2d8af9da6a5
This commit is contained in:
parent
5621786f75
commit
eaa5435416
|
@ -49,11 +49,6 @@ class Base(base.AuthMethodHandler):
|
|||
raise exception.Unauthorized(msg)
|
||||
|
||||
response_data['user_id'] = user_ref['id']
|
||||
auth_type = (request.auth_type or '').lower()
|
||||
|
||||
if 'kerberos' in CONF.token.bind and auth_type == 'negotiate':
|
||||
response_data.setdefault('bind', {})['kerberos'] = user_ref['name']
|
||||
|
||||
return base.AuthHandlerResponse(status=True, response_body=None,
|
||||
response_data=response_data)
|
||||
|
||||
|
|
|
@ -18,7 +18,6 @@ import six
|
|||
from keystone.auth.plugins import base
|
||||
from keystone.auth.plugins import mapped
|
||||
from keystone.common import provider_api
|
||||
from keystone.common import wsgi
|
||||
import keystone.conf
|
||||
from keystone import exception
|
||||
from keystone.i18n import _
|
||||
|
@ -103,8 +102,6 @@ def token_authenticate(request, token_ref):
|
|||
raise exception.ForbiddenAction(
|
||||
action=_('rescope a scoped token'))
|
||||
|
||||
wsgi.validate_token_bind(request.context_dict, token_ref)
|
||||
|
||||
# New tokens maintain the audit_id of the original token in the
|
||||
# chain (if possible) as the second element in the audit data
|
||||
# structure. Look for the last element in the audit data structure
|
||||
|
|
|
@ -41,7 +41,6 @@ from keystone.common import utils
|
|||
import keystone.conf
|
||||
from keystone import exception
|
||||
from keystone.i18n import _
|
||||
from keystone.models import token_model
|
||||
|
||||
|
||||
CONF = keystone.conf.CONF
|
||||
|
@ -57,68 +56,6 @@ JSON_ENCODE_CONTENT_TYPES = set(['application/json',
|
|||
'application/json-home'])
|
||||
|
||||
|
||||
def validate_token_bind(context, token_ref):
|
||||
bind_mode = CONF.token.enforce_token_bind
|
||||
|
||||
if bind_mode == 'disabled':
|
||||
return
|
||||
|
||||
if not isinstance(token_ref, token_model.KeystoneToken):
|
||||
raise exception.UnexpectedError(_('token reference must be a '
|
||||
'KeystoneToken type, got: %s') %
|
||||
type(token_ref))
|
||||
bind = token_ref.bind
|
||||
|
||||
# permissive and strict modes don't require there to be a bind
|
||||
permissive = bind_mode in ('permissive', 'strict')
|
||||
|
||||
if not bind:
|
||||
if permissive:
|
||||
# no bind provided and none required
|
||||
return
|
||||
else:
|
||||
msg = _('No bind information present in token.')
|
||||
LOG.info(msg)
|
||||
raise exception.Unauthorized(msg)
|
||||
|
||||
# get the named mode if bind_mode is not one of the known
|
||||
name = None if permissive or bind_mode == 'required' else bind_mode
|
||||
|
||||
if name and name not in bind:
|
||||
msg = (_('Named bind mode %(name)s not in bind information') %
|
||||
{'name': name})
|
||||
LOG.info(msg)
|
||||
raise exception.Unauthorized(msg)
|
||||
|
||||
for bind_type, identifier in bind.items():
|
||||
if bind_type == 'kerberos':
|
||||
if (context['environment'].get('AUTH_TYPE', '').lower() !=
|
||||
'negotiate'):
|
||||
msg = _('Kerberos credentials required and not present')
|
||||
LOG.info(msg)
|
||||
raise exception.Unauthorized(msg)
|
||||
|
||||
if context['environment'].get('REMOTE_USER') != identifier:
|
||||
msg = _('Kerberos credentials do not match those in bind')
|
||||
LOG.info(msg)
|
||||
raise exception.Unauthorized(msg)
|
||||
|
||||
LOG.info('Kerberos bind authentication successful')
|
||||
|
||||
elif bind_mode == 'permissive':
|
||||
LOG.debug(("Ignoring unknown bind (due to permissive mode): "
|
||||
"{%(bind_type)s: %(identifier)s}"), {
|
||||
'bind_type': bind_type,
|
||||
'identifier': identifier})
|
||||
else:
|
||||
msg = _('Could not verify unknown bind: {%(bind_type)s: '
|
||||
'%(identifier)s}') % {
|
||||
'bind_type': bind_type,
|
||||
'identifier': identifier}
|
||||
LOG.info(msg)
|
||||
raise exception.Unauthorized(msg)
|
||||
|
||||
|
||||
def best_match_language(req):
|
||||
"""Determine the best available locale.
|
||||
|
||||
|
|
|
@ -17,31 +17,18 @@ from oslo_log import versionutils
|
|||
|
||||
from keystone.conf import utils
|
||||
|
||||
|
||||
bind = cfg.ListOpt(
|
||||
'bind',
|
||||
default=[],
|
||||
help=utils.fmt("""
|
||||
This is a list of external authentication mechanisms which should add token
|
||||
binding metadata to tokens, such as `kerberos` or `x509`. Binding metadata is
|
||||
enforced according to the `[token] enforce_token_bind` option.
|
||||
"""))
|
||||
|
||||
enforce_token_bind = cfg.StrOpt(
|
||||
'enforce_token_bind',
|
||||
default='permissive',
|
||||
deprecated_since=versionutils.deprecated.PIKE,
|
||||
deprecated_for_removal=True,
|
||||
help=utils.fmt("""
|
||||
This controls the token binding enforcement policy on tokens presented to
|
||||
keystone with token binding metadata (as specified by the `[token] bind`
|
||||
option). `disabled` completely bypasses token binding validation. `permissive`
|
||||
and `strict` do not require tokens to have binding metadata (but will validate
|
||||
it if present), whereas `required` will always demand tokens to having binding
|
||||
metadata. `permissive` will allow unsupported binding metadata to pass through
|
||||
without validation (usually to be validated at another time by another
|
||||
component), whereas `strict` and `required` will demand that the included
|
||||
binding metadata be supported by keystone.
|
||||
This is a list of external authentication mechanisms which should add token
|
||||
binding metadata to tokens, such as `kerberos` or `x509`. Note that this option
|
||||
is deprecated as keystone no longer supports binding metadata to tokens
|
||||
directly. This option is silently ignored and will be removed in the future.
|
||||
This option no longer has any impact on the behavior of tokens and can be
|
||||
removed.
|
||||
"""))
|
||||
|
||||
expiration = cfg.IntOpt(
|
||||
|
@ -141,8 +128,6 @@ Defaults to two days.
|
|||
|
||||
GROUP_NAME = __name__.split('.')[-1]
|
||||
ALL_OPTS = [
|
||||
bind,
|
||||
enforce_token_bind,
|
||||
expiration,
|
||||
provider,
|
||||
caching,
|
||||
|
|
|
@ -39,10 +39,7 @@ class AuthContextMiddleware(provider_api.ProviderAPIMixin,
|
|||
kwargs_to_fetch_token = True
|
||||
|
||||
def __init__(self, app):
|
||||
bind = CONF.token.enforce_token_bind
|
||||
super(AuthContextMiddleware, self).__init__(app,
|
||||
log=LOG,
|
||||
enforce_token_bind=bind)
|
||||
super(AuthContextMiddleware, self).__init__(app, log=LOG)
|
||||
|
||||
def fetch_token(self, token, **kwargs):
|
||||
try:
|
||||
|
|
|
@ -1,198 +0,0 @@
|
|||
# Copyright 2013 OpenStack Foundation
|
||||
#
|
||||
# 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.
|
||||
|
||||
import copy
|
||||
import uuid
|
||||
|
||||
from keystone.common import wsgi
|
||||
from keystone import exception
|
||||
from keystone.models import token_model
|
||||
from keystone.tests import unit
|
||||
from keystone.tests.unit import test_token_provider
|
||||
|
||||
|
||||
KERBEROS_BIND = 'USER@REALM'
|
||||
ANY = 'any'
|
||||
|
||||
|
||||
class BindTest(unit.TestCase):
|
||||
"""Test binding tokens to a Principal.
|
||||
|
||||
Even though everything in this file references kerberos the same concepts
|
||||
will apply to all future binding mechanisms.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(BindTest, self).setUp()
|
||||
self.TOKEN_BIND_KERB = copy.deepcopy(
|
||||
test_token_provider.SAMPLE_V3_TOKEN)
|
||||
self.TOKEN_BIND_KERB['token']['bind'] = {'kerberos': KERBEROS_BIND}
|
||||
self.TOKEN_BIND_UNKNOWN = copy.deepcopy(
|
||||
test_token_provider.SAMPLE_V3_TOKEN)
|
||||
self.TOKEN_BIND_UNKNOWN['token']['bind'] = {'FOO': 'BAR'}
|
||||
self.TOKEN_BIND_NONE = copy.deepcopy(
|
||||
test_token_provider.SAMPLE_V3_TOKEN)
|
||||
|
||||
self.ALL_TOKENS = [self.TOKEN_BIND_KERB, self.TOKEN_BIND_UNKNOWN,
|
||||
self.TOKEN_BIND_NONE]
|
||||
|
||||
def assert_kerberos_bind(self, tokens, bind_level,
|
||||
use_kerberos=True, success=True):
|
||||
if not isinstance(tokens, dict):
|
||||
for token in tokens:
|
||||
self.assert_kerberos_bind(token, bind_level,
|
||||
use_kerberos=use_kerberos,
|
||||
success=success)
|
||||
elif use_kerberos == ANY:
|
||||
for val in (True, False):
|
||||
self.assert_kerberos_bind(tokens, bind_level,
|
||||
use_kerberos=val, success=success)
|
||||
else:
|
||||
context = {'environment': {}}
|
||||
self.config_fixture.config(group='token',
|
||||
enforce_token_bind=bind_level)
|
||||
|
||||
if use_kerberos:
|
||||
context['environment']['REMOTE_USER'] = KERBEROS_BIND
|
||||
context['environment']['AUTH_TYPE'] = 'Negotiate'
|
||||
|
||||
# NOTE(morganfainberg): This assumes a V3 token.
|
||||
token_ref = token_model.KeystoneToken(
|
||||
token_id=uuid.uuid4().hex,
|
||||
token_data=tokens)
|
||||
|
||||
if not success:
|
||||
self.assertRaises(exception.Unauthorized,
|
||||
wsgi.validate_token_bind,
|
||||
context, token_ref)
|
||||
else:
|
||||
wsgi.validate_token_bind(context, token_ref)
|
||||
|
||||
# DISABLED
|
||||
|
||||
def test_bind_disabled_with_kerb_user(self):
|
||||
self.assert_kerberos_bind(self.ALL_TOKENS,
|
||||
bind_level='disabled',
|
||||
use_kerberos=ANY,
|
||||
success=True)
|
||||
|
||||
# PERMISSIVE
|
||||
|
||||
def test_bind_permissive_with_kerb_user(self):
|
||||
self.assert_kerberos_bind(self.TOKEN_BIND_KERB,
|
||||
bind_level='permissive',
|
||||
use_kerberos=True,
|
||||
success=True)
|
||||
|
||||
def test_bind_permissive_with_regular_token(self):
|
||||
self.assert_kerberos_bind(self.TOKEN_BIND_NONE,
|
||||
bind_level='permissive',
|
||||
use_kerberos=ANY,
|
||||
success=True)
|
||||
|
||||
def test_bind_permissive_without_kerb_user(self):
|
||||
self.assert_kerberos_bind(self.TOKEN_BIND_KERB,
|
||||
bind_level='permissive',
|
||||
use_kerberos=False,
|
||||
success=False)
|
||||
|
||||
def test_bind_permissive_with_unknown_bind(self):
|
||||
self.assert_kerberos_bind(self.TOKEN_BIND_UNKNOWN,
|
||||
bind_level='permissive',
|
||||
use_kerberos=ANY,
|
||||
success=True)
|
||||
|
||||
# STRICT
|
||||
|
||||
def test_bind_strict_with_regular_token(self):
|
||||
self.assert_kerberos_bind(self.TOKEN_BIND_NONE,
|
||||
bind_level='strict',
|
||||
use_kerberos=ANY,
|
||||
success=True)
|
||||
|
||||
def test_bind_strict_with_kerb_user(self):
|
||||
self.assert_kerberos_bind(self.TOKEN_BIND_KERB,
|
||||
bind_level='strict',
|
||||
use_kerberos=True,
|
||||
success=True)
|
||||
|
||||
def test_bind_strict_without_kerb_user(self):
|
||||
self.assert_kerberos_bind(self.TOKEN_BIND_KERB,
|
||||
bind_level='strict',
|
||||
use_kerberos=False,
|
||||
success=False)
|
||||
|
||||
def test_bind_strict_with_unknown_bind(self):
|
||||
self.assert_kerberos_bind(self.TOKEN_BIND_UNKNOWN,
|
||||
bind_level='strict',
|
||||
use_kerberos=ANY,
|
||||
success=False)
|
||||
|
||||
# REQUIRED
|
||||
|
||||
def test_bind_required_with_regular_token(self):
|
||||
self.assert_kerberos_bind(self.TOKEN_BIND_NONE,
|
||||
bind_level='required',
|
||||
use_kerberos=ANY,
|
||||
success=False)
|
||||
|
||||
def test_bind_required_with_kerb_user(self):
|
||||
self.assert_kerberos_bind(self.TOKEN_BIND_KERB,
|
||||
bind_level='required',
|
||||
use_kerberos=True,
|
||||
success=True)
|
||||
|
||||
def test_bind_required_without_kerb_user(self):
|
||||
self.assert_kerberos_bind(self.TOKEN_BIND_KERB,
|
||||
bind_level='required',
|
||||
use_kerberos=False,
|
||||
success=False)
|
||||
|
||||
def test_bind_required_with_unknown_bind(self):
|
||||
self.assert_kerberos_bind(self.TOKEN_BIND_UNKNOWN,
|
||||
bind_level='required',
|
||||
use_kerberos=ANY,
|
||||
success=False)
|
||||
|
||||
# NAMED
|
||||
|
||||
def test_bind_named_with_regular_token(self):
|
||||
self.assert_kerberos_bind(self.TOKEN_BIND_NONE,
|
||||
bind_level='kerberos',
|
||||
use_kerberos=ANY,
|
||||
success=False)
|
||||
|
||||
def test_bind_named_with_kerb_user(self):
|
||||
self.assert_kerberos_bind(self.TOKEN_BIND_KERB,
|
||||
bind_level='kerberos',
|
||||
use_kerberos=True,
|
||||
success=True)
|
||||
|
||||
def test_bind_named_without_kerb_user(self):
|
||||
self.assert_kerberos_bind(self.TOKEN_BIND_KERB,
|
||||
bind_level='kerberos',
|
||||
use_kerberos=False,
|
||||
success=False)
|
||||
|
||||
def test_bind_named_with_unknown_bind(self):
|
||||
self.assert_kerberos_bind(self.TOKEN_BIND_UNKNOWN,
|
||||
bind_level='kerberos',
|
||||
use_kerberos=ANY,
|
||||
success=False)
|
||||
|
||||
def test_bind_named_with_unknown_scheme(self):
|
||||
self.assert_kerberos_bind(self.ALL_TOKENS,
|
||||
bind_level='unknown',
|
||||
use_kerberos=ANY,
|
||||
success=False)
|
|
@ -2335,55 +2335,6 @@ class TokenAPITests(object):
|
|||
auth_info,
|
||||
auth_context)
|
||||
|
||||
def test_bind_not_set_with_remote_user(self):
|
||||
self.config_fixture.config(group='token', bind=[])
|
||||
auth_data = self.build_authentication_request()
|
||||
remote_user = self.default_domain_user['name']
|
||||
self.public_app.extra_environ.update({'REMOTE_USER': remote_user,
|
||||
'AUTH_TYPE': 'Negotiate'})
|
||||
r = self.v3_create_token(auth_data)
|
||||
token = self.assertValidUnscopedTokenResponse(r)
|
||||
self.assertNotIn('bind', token)
|
||||
|
||||
def test_verify_with_bound_token(self):
|
||||
self.config_fixture.config(group='token', bind='kerberos')
|
||||
auth_data = self.build_authentication_request(
|
||||
project_id=self.project['id'])
|
||||
remote_user = self.default_domain_user['name']
|
||||
self.public_app.extra_environ.update({'REMOTE_USER': remote_user,
|
||||
'AUTH_TYPE': 'Negotiate'})
|
||||
|
||||
token = self.get_requested_token(auth_data)
|
||||
headers = {'X-Subject-Token': token}
|
||||
r = self.get('/auth/tokens', headers=headers, token=token)
|
||||
token = self.assertValidProjectScopedTokenResponse(r)
|
||||
self.assertEqual(self.default_domain_user['name'],
|
||||
token['bind']['kerberos'])
|
||||
|
||||
def test_auth_with_bind_token(self):
|
||||
self.config_fixture.config(group='token', bind=['kerberos'])
|
||||
|
||||
auth_data = self.build_authentication_request()
|
||||
remote_user = self.default_domain_user['name']
|
||||
self.public_app.extra_environ.update({'REMOTE_USER': remote_user,
|
||||
'AUTH_TYPE': 'Negotiate'})
|
||||
r = self.v3_create_token(auth_data)
|
||||
|
||||
# the unscoped token should have bind information in it
|
||||
token = self.assertValidUnscopedTokenResponse(r)
|
||||
self.assertEqual(remote_user, token['bind']['kerberos'])
|
||||
|
||||
token = r.headers.get('X-Subject-Token')
|
||||
|
||||
# using unscoped token with remote user succeeds
|
||||
auth_params = {'token': token, 'project_id': self.project_id}
|
||||
auth_data = self.build_authentication_request(**auth_params)
|
||||
r = self.v3_create_token(auth_data)
|
||||
token = self.assertValidProjectScopedTokenResponse(r)
|
||||
|
||||
# the bind information should be carried over from the original token
|
||||
self.assertEqual(remote_user, token['bind']['kerberos'])
|
||||
|
||||
def test_fetch_expired_allow_expired(self):
|
||||
self.config_fixture.config(group='token',
|
||||
expiration=10,
|
||||
|
@ -2582,28 +2533,6 @@ class TestFernetTokenAPIs(test_v3.RestfulTestCase, TokenAPITests,
|
|||
self._validate_token(tampered_token,
|
||||
expected_status=http_client.NOT_FOUND)
|
||||
|
||||
def test_verify_with_bound_token(self):
|
||||
self.config_fixture.config(group='token', bind='kerberos')
|
||||
auth_data = self.build_authentication_request(
|
||||
project_id=self.project['id'])
|
||||
remote_user = self.default_domain_user['name']
|
||||
self.public_app.extra_environ.update({'REMOTE_USER': remote_user,
|
||||
'AUTH_TYPE': 'Negotiate'})
|
||||
# Bind not current supported by Fernet, see bug 1433311.
|
||||
self.v3_create_token(auth_data,
|
||||
expected_status=http_client.NOT_IMPLEMENTED)
|
||||
|
||||
def test_auth_with_bind_token(self):
|
||||
self.config_fixture.config(group='token', bind=['kerberos'])
|
||||
|
||||
auth_data = self.build_authentication_request()
|
||||
remote_user = self.default_domain_user['name']
|
||||
self.public_app.extra_environ.update({'REMOTE_USER': remote_user,
|
||||
'AUTH_TYPE': 'Negotiate'})
|
||||
# Bind not current supported by Fernet, see bug 1433311.
|
||||
self.v3_create_token(auth_data,
|
||||
expected_status=http_client.NOT_IMPLEMENTED)
|
||||
|
||||
def test_trust_scoped_token_is_invalid_after_disabling_trustor(self):
|
||||
# NOTE(amakarov): have to override this test for non-persistent tokens
|
||||
# as TokenNotFound exception makes no sense for those.
|
||||
|
@ -3638,32 +3567,6 @@ class AuthExternalDomainBehavior(object):
|
|||
api.authenticate(request, auth_info, auth_context)
|
||||
self.assertEqual(self.user['id'], auth_context['user_id'])
|
||||
|
||||
def test_project_id_scoped_with_remote_user(self):
|
||||
self.config_fixture.config(group='token', bind=['kerberos'])
|
||||
auth_data = self.build_authentication_request(
|
||||
project_id=self.project['id'],
|
||||
kerberos=self.kerberos)
|
||||
remote_user = self.user['name']
|
||||
remote_domain = self.domain['name']
|
||||
self.public_app.extra_environ.update({'REMOTE_USER': remote_user,
|
||||
'REMOTE_DOMAIN': remote_domain,
|
||||
'AUTH_TYPE': 'Negotiate'})
|
||||
r = self.v3_create_token(auth_data)
|
||||
token = self.assertValidProjectScopedTokenResponse(r)
|
||||
self.assertEqual(self.user['name'], token['bind']['kerberos'])
|
||||
|
||||
def test_unscoped_bind_with_remote_user(self):
|
||||
self.config_fixture.config(group='token', bind=['kerberos'])
|
||||
auth_data = self.build_authentication_request(kerberos=self.kerberos)
|
||||
remote_user = self.user['name']
|
||||
remote_domain = self.domain['name']
|
||||
self.public_app.extra_environ.update({'REMOTE_USER': remote_user,
|
||||
'REMOTE_DOMAIN': remote_domain,
|
||||
'AUTH_TYPE': 'Negotiate'})
|
||||
r = self.v3_create_token(auth_data)
|
||||
token = self.assertValidUnscopedTokenResponse(r)
|
||||
self.assertEqual(self.user['name'], token['bind']['kerberos'])
|
||||
|
||||
|
||||
class TestAuthExternalDefaultDomain(object):
|
||||
content_type = 'json'
|
||||
|
@ -3697,30 +3600,6 @@ class TestAuthExternalDefaultDomain(object):
|
|||
self.assertEqual(self.default_domain_user['id'],
|
||||
auth_context['user_id'])
|
||||
|
||||
def test_project_id_scoped_with_remote_user(self):
|
||||
self.config_fixture.config(group='token', bind=['kerberos'])
|
||||
auth_data = self.build_authentication_request(
|
||||
project_id=self.default_domain_project['id'],
|
||||
kerberos=self.kerberos)
|
||||
remote_user = self.default_domain_user['name']
|
||||
self.public_app.extra_environ.update({'REMOTE_USER': remote_user,
|
||||
'AUTH_TYPE': 'Negotiate'})
|
||||
r = self.v3_create_token(auth_data)
|
||||
token = self.assertValidProjectScopedTokenResponse(r)
|
||||
self.assertEqual(self.default_domain_user['name'],
|
||||
token['bind']['kerberos'])
|
||||
|
||||
def test_unscoped_bind_with_remote_user(self):
|
||||
self.config_fixture.config(group='token', bind=['kerberos'])
|
||||
auth_data = self.build_authentication_request(kerberos=self.kerberos)
|
||||
remote_user = self.default_domain_user['name']
|
||||
self.public_app.extra_environ.update({'REMOTE_USER': remote_user,
|
||||
'AUTH_TYPE': 'Negotiate'})
|
||||
r = self.v3_create_token(auth_data)
|
||||
token = self.assertValidUnscopedTokenResponse(r)
|
||||
self.assertEqual(self.default_domain_user['name'],
|
||||
token['bind']['kerberos'])
|
||||
|
||||
|
||||
class TestAuthJSONExternal(test_v3.RestfulTestCase):
|
||||
content_type = 'json'
|
||||
|
|
|
@ -9,3 +9,8 @@ other:
|
|||
Removed support for direct import of authentication drivers. If you're
|
||||
using full path names for authentication methods in configuration, please
|
||||
update your configuration to use the corresponding namespaces.
|
||||
- >
|
||||
[`blueprint removed-as-of-rocky <https://blueprints.launchpad.net/keystone/+spec/removed-as-of-rocky>`_]
|
||||
Removed support for token bind operations, which were supported by the
|
||||
``uuid``, ``pki``, and ``pkiz`` token providers. Support for this
|
||||
feature was deprecated in Pike.
|
||||
|
|
Loading…
Reference in New Issue