Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[2.8] [Ldap] Added support for LDAP (New Component + integration in the Security Component). #14602

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Sep 28, 2015
Merged

Conversation

csarrazi
Copy link
Contributor

Q A
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? no
Tests pass? not yet
Fixed tickets -
License MIT
Doc PR not yet

Current state:

  • Implement logic
  • Post-review tuning and stabilization
  • Fix tests

This PR is a follow-up to #5189, which was in a stand-still for a few years now. It tries to fix the remaining issues which were mentioned in the discussion.

There are still a few issues with the PR, as it is. For example, it introduces two new firewall factories, whereas the base factories (form_login and http_basic) could simply introduce new configuration options.

Also, for a user to use an LDAP server as an authentication provider, he first needs to define a service which should be an instance of Symfony\Component\Security\Ldap\Ldap.

For example:

services:
    my_ldap:
        class: Symfony\Component\Security\Ldap\Ldap
        arguments: [ "ldap.mydomain.tld" ]

Then, in security.yml, this service can be used in both the user provider and the firewalls:

security:
    encoders:
        Symfony\Component\Security\Core\User\User: plaintext

    role_hierarchy:
        ROLE_ADMIN:       ROLE_USER
        ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]

    providers:
        ldap_users:
            ldap:
                service: my_ldap
                base_dn: dc=MyDomain,dc=tld
                search_dn: CN=My User,OU=Users,DC=MyDomain,DC=tld
                search_password: p455w0rd
                filter: (sAMAccountName={username})
                default_roles: ROLE_USER

    firewalls:
        dev:
            pattern:  ^/(_(profiler|wdt)|css|images|js)/
            security: false
        demo_login:
            pattern:  ^/login$
            security: false
        api:
            provider: ldap_users
            stateless: true
            pattern:    ^/api
            http_basic_ldap:
                service: my_ldap
                dn_string: "{username}@MYDOMAIN"
        demo_secured_area:
            provider: ldap_users
            pattern:    ^/
            logout:
                path:   logout
                target: login
            form_login_ldap:
                service: my_ldap
                dn_string: CN={username},OU=Users,DC=MyDomain,DC=tld
                check_path: login_check
                login_path: login

* @author Grégoire Pineau <[email protected]>
* @author Charles Sarrazin <[email protected]>
*/
class FormLoginLdapFactory extends FormLoginFactory
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're making it impossible like this to attach a custom authenticator. Can you extend the SimpleFormFactory instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These factories are only to be used with LDAP, and actually override the authentication provider to be the LDAP bind authentication provider. As it already handles authentication, I don't really see why you would want to attach a custom authenticator.

Can you provide a use case?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example if a user account is disabled, a certain token is not supplied etc. LDAP should be nothing more than a specific user provider in my opinion. Instead of fetching it from the database, you fetch it from ldap.

For me it's currently impossible to do the following:

  • Custom validation on the user account
  • Restrict logins if a user account is in a certain ldap-group
  • Adding a 3rd required field, for example specifying a second "username", where the uniqueness lies in (for example) vendor + username

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This issue should start to be addressed in #14673 (with the new guard authentication system), and maybe #14713.

This feature will be adapted for the new guard system, and improved once these PRs. Depending on when (and whether) these two PRs will be merged, a new PR should be made to update the legacy authentication providers. Until then, I will fix the tests, unless there actually is another issue.

Ping @iltar @weaverryan

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the guard PR gets merged and we can build this on top of that, that's awesome! Realistically, since security is so complex, I imagine that we (the community) will make a push on all these security-related PR's at the same time and decide how it will all end up. For now, I agree with making this PR as solid as possible without #14673 and #14713... though I hope that'll change ;).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. I keep the PR as is. I'll fix the tests, and that should do it.

Of course, I'm open for any suggestion.

Ping @iltar @stof @weaverryan

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think core features in the Security component should still be built with core features. Guard is more for end-users.

@@ -48,6 +48,7 @@
"symfony/expression-language": "For using the expression voter",
"ircmaxell/password-compat": "For using the BCrypt password encoder in PHP <5.5"
},
"minimum-stability": "dev",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this really required?

Can you add a suggests for the LDAP in here?
http://php.net/manual/en/ldap.requirements.php

Maybe it's an idea to throw an exception in the SecurityExtension if LDAP is configured but not installed on the system?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the minimum-stability, I don't think so. It's simply a remnant of the previous PR, so I guess I'll fix that. No problem for the LDAP suggest (for the extension), too. :)

As for the SecurityExtension triggering an exception if the LDAP extension is not installed, I don't think it's necessary. If you wish to, you can use the PEAR LDAP2 implementation, which is a full-PHP implementation. And the default implementation (which uses the LDAP extension) already throws an exception if the extension is not loaded. :)

@csarrazi
Copy link
Contributor Author

@jakzal You can also add the Feature label, by the way, as it is a new feature! :)

@csarrazi
Copy link
Contributor Author

csarrazi commented Jul 3, 2015

Ok. I'm running into an issue: Should LDAP classes be in Symfony\Component\Security\Core, or in their own namespace? Currently, that's the only thing preventing the tests from passing.

Any advice on this, @weaverryan, @stof, @iltar?

@linaori
Copy link
Contributor

linaori commented Jul 3, 2015

If the LDAP connection and such is generic enough, it might not belong in the Security component at all.

I'm still not satisfied with how this is going to be configured though, fetching your user from LDAP should be nothing more than changing your user provider. This will make it possible to use it with any authentication provider.

@csarrazi
Copy link
Contributor Author

csarrazi commented Jul 3, 2015

It's already the case. Two things are implemented in this PR:

  • A user provider
  • An authentication provider

Each of them are independent from each other. This means that you can fetch your users from your LDAP server, while checking their password as you would normally (should your LDAP provide the password hash), using ANY authentication provider. But in this case, you won't check the user's password using ldap_bind().

If you wish to check the user's password against an LDAP server, then you use the authentication provider. This provider lets you use any user provider, while checking the user's password against an LDAP server.

This is the case because of Symfony's security component suffers from a limitation, which is that the password check mechanism is directly implemented inside the authentication providers. And I don't want to adress two things in a single PR.

First, LDAP support. Second, refactor the security component, to separate the password check from the authentication providers.

If you feel that this is really needed now, I'll separate this PR into two different PRs:

  • A PR to add an LDAP component, as well as the LDAP user provider (without support for ldap_bind(), as this means that the user provider needs to be aware of the password, which is a no go)
  • A PR to separate the password check logic from all authentication providers, and implement the LDAP password check mechanism as a password checker.

What do you think about this?

@linaori
Copy link
Contributor

linaori commented Jul 3, 2015

The user providers meant to merely provide a user to authenticate, when you fetch the user, it contains a password. This authentication is method is fine and the password check IMO as well as not every provider needs a password. To verify if the user is correct, you simply verify the password with the encoding strategy selected by the configuration, which will be "plain" most likely.

You're correct that it's absolutely not desired to store the password (plain) in the user as this is very risky. There's a solution for this, you'll have to implement the eraseCredentials() method in your user.

If you need to call another method, you can always listen to an event that's fired when the login was successful and bind there.

@jvasseur
Copy link
Contributor

jvasseur commented Jul 3, 2015

Except LDAP auth doesn't work like this. Most of the time you don't have access to the user password, instead you connect to the server with the user credential and check if the login attempt succeeded or not.

@csarrazi
Copy link
Contributor Author

csarrazi commented Jul 3, 2015

Ping @fabpot Should we have an LDAP component?

@fabpot
Copy link
Member

fabpot commented Jul 3, 2015

@csarrazi If the classes are useful by themselves and are not trivial, I would say yes.

@csarrazi
Copy link
Contributor Author

csarrazi commented Jul 3, 2015

Ok. This would indeed resolve part of the issue, which would let us add the component in the require-dev section of composer.json, fixing the test issue.

The class is not trivial by itself, as it provides a shim for ldap_escape, by back-porting the function from PHP 5.5 (otherwise, it would have meant bumping the PHP version to 5.5 in 2.8, which is a no-go). As the version requirement for Symfony 3.0 is 5.5.0, I will also add a deprecation notice in the class.

@lyrixx
Copy link
Member

lyrixx commented Sep 23, 2015

@iltar

I'm still not satisfied with how this is going to be configured though, fetching your user from LDAP should be nothing more than changing your user provider. This will make it possible to use it with any authentication provider.

Did you already work with LDAP ? Because it looks like it's not the case. Symfony Security default workflow does not work with LDAP Authentication workflow.

@linaori
Copy link
Contributor

linaori commented Sep 23, 2015

@lyrixx I have worked with LDAP (and different ldap bundles), but I'm also looking from a DX point of view. There should be enough extension points to solve this while leaving it 'configurable' by the developer.

@csarrazi
Copy link
Contributor Author

There are two different concerns here:

  • Fetching a user from the LDAP (which is the responsibility of the user provider)
  • Validating a user's credentials against the LDAP (which is the responsibility of the authentication provider)

These are two different things, and this PR works with any combination of these use cases:

  • Authenticating using ldap_bind and fetching users from another storage (Database, for example).
  • Authenticating using ldap_bind and fetching users from an LDAP server.
  • Authenticating using password comparison and fetching users from another storage (Database, for example).
  • Authenticating using password comparison and fetching users from an LDAP server.

As mentioned earlier, these are all different use cases.

You may want to authenticate against your LDAP server, while fetching users from a database. Just like you would, if you wanted to fetch users from a local database, while validating the user's credentials from an OAuth 2.0 identity provider.

Or you may want to fetch your users from an LDAP server. In this case, you can check the user's credentials using one of two strategies:

  • Comparing the user's password (from the LDAP user provider) against the information provided in the request (if using http-basic or a login form).
  • Binding the user's credentials (directly from the request's info) against an LDAP server.

Like mentioned earlier, if we want to avoid duplication (and duplicating the authentication providers), we would need to separate the authentication check logic from the authentication provider (http-basic, etc.), so we can use different authentication check types, with a single authentication scheme (http-basic, login form, etc.).

However, we need to think a bit more about this, as it would probably induce BC with the 2.x branch of Symfony.

*/
protected function retrieveUser($username, UsernamePasswordToken $token)
{
return $this->userProvider->loadUserByUsername($username);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about adding theses lines ?

        if ('NONE_PROVIDED' === $uuid) {
            throw new UsernameNotFoundException('Username can not be null');
        }

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, I'm not sure about this one. I don't know where we should put this piece of code.
For exemple, if you are using entity user provider, with guid type for the username column, and with postgres, a Postgres sql exception will be thrown. So one will have to implements a custom entity provider to avoid this error.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By $uuid, you mean $username?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oups. Yes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. Rather than using an UsernameNotFoundException, I'll use an InvalidArgumentException. Is that okay with you?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know. This method should throw an UsernameNotFoundException

@lyrixx
Copy link
Member

lyrixx commented Sep 23, 2015

Does someone know how to setup travis + a ldap server ?
I see this but it's not really easy

@csarrazi csarrazi changed the title [Ldap] Added support for LDAP (New Component + integration in the Security Component). [2.8] [Ldap] Added support for LDAP (New Component + integration in the Security Component). Sep 25, 2015
return;
}

$ldap = new LdapClient();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would use setUp method to create the SUT. But it's a matter of taste.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have done this, if there were more than one test method ;)

@csarrazi
Copy link
Contributor Author

Ping @fabpot @Tobion @dunglas

@csarrazi
Copy link
Contributor Author

Tests seem okay. However, some unrelated tests seem to break randomly on travis and/or appveyor.

@fabpot
Copy link
Member

fabpot commented Sep 28, 2015

Sorry about that but it looks like a rebase is needed.

@csarrazi
Copy link
Contributor Author

No problem! I'll also include @lyrixx 's changes regarding the setUp() method and the test skipping when the ldap extension is not installed.

@@ -35,6 +35,7 @@ before_install:
- if [[ "$TRAVIS_PHP_VERSION" = 5.* ]]; then (pecl install -f memcached-2.1.0 && echo "extension = memcache.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini) || echo "Let's continue without memcache extension"; fi;
- if [[ "$TRAVIS_PHP_VERSION" = 5.* ]] && [ "$deps" = "no" ]; then (cd src/Symfony/Component/Debug/Resources/ext && phpize && ./configure && make && echo "extension = $(pwd)/modules/symfony_debug.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini); fi;
- if [[ "$TRAVIS_PHP_VERSION" != "hhvm" ]]; then php -i; fi;
- if [[ "$TRAVIS_PHP_VERSION" != "hhvm" ]]; then echo "extension = ldap.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini; fi;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The LDAP extension is available without any installation?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. It seems that the LDAP extension is available by default in Travis' PHP build.

See travis-ci/travis-ci#1096

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indeed, 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, and this is certain, as I added the skip tests only after checking that they passed on travis first, and because of appveyor launching tests with and without PHP extensions.

@fabpot
Copy link
Member

fabpot commented Sep 28, 2015

Thank you @csarrazi.

@fabpot fabpot merged commit 60b9f2e into symfony:2.8 Sep 28, 2015
fabpot added a commit that referenced this pull request Sep 28, 2015
…ntegration in the Security Component). (csarrazi, lyrixx)

This PR was merged into the 2.8 branch.

Discussion
----------

[2.8] [Ldap] Added support for LDAP (New Component + integration in the Security Component).

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | not yet
| Fixed tickets | -
| License       | MIT
| Doc PR        | not yet

Current state:

- [x] Implement logic
- [x] Post-review tuning and stabilization
- [x] Fix tests

This PR is a follow-up to #5189, which was in a stand-still for a few years now. It tries to fix the remaining issues which were mentioned in the discussion.

There are still a few issues with the PR, as it is. For example, it introduces two new firewall factories, whereas the base factories (`form_login` and `http_basic`) could simply introduce new configuration options.

Also, for a user to use an LDAP server as an authentication provider, he first needs to define a service which should be an instance of `Symfony\Component\Security\Ldap\Ldap`.

For example:

```yml
services:
    my_ldap:
        class: Symfony\Component\Security\Ldap\Ldap
        arguments: [ "ldap.mydomain.tld" ]
```

Then, in `security.yml`, this service can be used in both the user provider and the firewalls:

```yml
security:
    encoders:
        Symfony\Component\Security\Core\User\User: plaintext

    role_hierarchy:
        ROLE_ADMIN:       ROLE_USER
        ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]

    providers:
        ldap_users:
            ldap:
                service: my_ldap
                base_dn: dc=MyDomain,dc=tld
                search_dn: CN=My User,OU=Users,DC=MyDomain,DC=tld
                search_password: p455w0rd
                filter: (sAMAccountName={username})
                default_roles: ROLE_USER

    firewalls:
        dev:
            pattern:  ^/(_(profiler|wdt)|css|images|js)/
            security: false
        demo_login:
            pattern:  ^/login$
            security: false
        api:
            provider: ldap_users
            stateless: true
            pattern:    ^/api
            http_basic_ldap:
                service: my_ldap
                dn_string: "{username}@mydomain"
        demo_secured_area:
            provider: ldap_users
            pattern:    ^/
            logout:
                path:   logout
                target: login
            form_login_ldap:
                service: my_ldap
                dn_string: CN={username},OU=Users,DC=MyDomain,DC=tld
                check_path: login_check
                login_path: login
```

Commits
-------

60b9f2e Implemented LDAP authentication and LDAP user provider
1c964b9 Introducing the LDAP component
@csarrazi csarrazi deleted the feat-ldap branch September 28, 2015 11:39
@@ -0,0 +1,49 @@
<?php

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

header class is missing

fabpot added a commit that referenced this pull request Sep 28, 2015
This PR was merged into the 2.8 branch.

Discussion
----------

[Ldap] add some missing license file headers

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | no
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #14602 (comment)
| License       | MIT
| Doc PR        |

Commits
-------

2b90fcf [Ldap] add some missing license file headers
* @param bool $useStartTls
* @param bool $optReferrals
*/
public function __construct($host = null, $port = 389, $version = 3, $useSsl = false, $useStartTls = false, $optReferrals = false)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's impossible to specify additional options, such as timeout, debug level, etc... As this component should be used as a standalone, I suggest to put options here or make an interface to adjust each option, available for ldap_set_option

Example:

$ldap = new LdapClient();
$ldap->setNetworkTimeout(10);
$ldap->setDebugLevel(7);
$ldap->setUseSsl(true);
$ldap->setProtocolVersion(3);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer a configuration object per connection, it's possible to have multiple connections. Optionally the Option component could be used to build this.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@iltar yes, nice catch. In my case we have connections to the several hosts and one "aggregated" client to query an information. But this LdapClient is simple enough and should be used as single entry point for single connection.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could separate the Client from the Connection implementation, if needed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having a configuration object seems like a good idea. The long list of arguments here is indeed a problem.

return $this->doEscape($subject, $ignore, $flags);
}

private function connect()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that this method should be protected, or, at least, allow to configure all options for connection.
Why I want to make it protected: I can define a class with reconnection logic in case I needed a fail-tolerance solution, additionally, we always enable timeouts for connections (1second)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm repeating myself, but the connection logic will be moved out from the client. So indeed, you will be able to configure all connection options (as well as connection handling) the Connection class.

@fabpot fabpot mentioned this pull request Nov 16, 2015
@csarrazi csarrazi mentioned this pull request Nov 25, 2015
5 tasks
fabpot added a commit that referenced this pull request Feb 14, 2016
This PR was merged into the 3.1-dev branch.

Discussion
----------

[Ldap] Improving the LDAP component

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | yes
| Deprecations? | no
| Tests pass?   | no
| Fixed tickets | #14602
| License       | MIT
| Doc PR        | not yet

This PR will address a few issues mentioned in #14602.

* [x] Integrate the Config component in order to simplify the client's configuration
* [x] Separate Connection handling from the Client
* [x] Support for multiple drivers
* [x] Add functional tests
* [x] Update Security component

Commits
-------

34d3c85 Added compatibility layer for previous version of the Security component
81cb79b Improved the Ldap Component
@jianzhong
Copy link

@csarrazi Would it be possible to use the same login credential for both form_login_ldap authentication provider and ldap user provider? No sure if this has been addressed before, but I couldn't find any information about it after googling a few days. This LDAP component seems requiring 2 LDAP accounts to authenticate users: a fixed site-wise admin account as search_dn for ldap user provider, and a user account to be logging in for the form_login_ldap. This would cause issue if it is not possible to get a fixed admin credential from the organisation or if the admin password would change regularly. Seeing other framework dealing with ldap authentication only requires one ldap user credential, I wonder if this ldap component has a way to do the same? Please advice, thanks in advance.

@csarrazi
Copy link
Contributor Author

csarrazi commented Jan 5, 2017

Hi @jianzhong.

The Ldap component itself does not offer any limitation of this kind. With your own logic, you could actually build your own custom authenticator, which could bind to the LDAP server, then search for a user with the same credentials using a custom user provider.

This type of feature may be implemented within the security component in some time. For that, we could provide an option to disable re-binding the user in the user provider, and provide another option to configure when to bind the user against the ldap server.

Also, this would only work in a few select cases:

  • Authentication with a stateless mechanism which transfers plain text credentials (HTTP-Basic)
  • Authentication mechanisms which only load the user's information a single time, and then stores them in another storage for refresh (session or database).

Re-binding the same user against the ldap server between two different requests without asking for credentials won't (and shouldn't) be supported.

This mostly has to do with the security component, and how PHP works in general, when using apache's mod_php or php-fpm. Whenever a request is sent to the server, the whole userland context (what the PHP script is doing) is discarded, and flushed from memory. Many database extensions (MySQL or MongoDB, for example) actually implement connection pooling and/or provide the ability to keep connections alive between requests, but it is not the case for PHP's LDAP extension.
Moreover, the LDAP extension actually requires the password to be transferred in plain text, which means that the password would need to be available in clear text for every authenticated request, meaning that the user's password should be stored either in the session or the database using two-way encryption. This causes a security risk, or limits us solely to authentication mechanisms which transfer the credentials in plain text for every request (HTTP-Basic, for example), or in a format which can be decrypted to transfer the password in plain text to the LADP server.

@jianzhong
Copy link

Thanks @csarrazi for your reply and pointing me to the right direction.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.