Skip to content
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

Added a "Forgotten password" maker #359

Open
wants to merge 1 commit into
base: master
from

Conversation

Projects
None yet
7 participants
@romaricdrigon
Copy link

commented Jan 31, 2019

Hello,

Having a password reset feature ("forgotten password") is a common feature of a web application. Maker bundle already has a User registration maker, it makes sense imo to add this too. Otherwise users are left either to code it manually (long), or to ditch Maker commands for FOSUserBundle.

I started implementing such a maker. It is heavily inspired from other Registration maker regarding style, and from FOSUserBundle process. Right now this is a work in progress, but I wanted to propose the idea & to discuss code style.

To do:

  • first working POC
  • add (minimal) view
  • add documentation describing the command
  • add tests - I got those are functional tests over the generated code?
  • squash & rebase everything

Points to be discussed:

  • is the controller right now looking good? I'm trying to have something minimalistic doing the job right, and easily modifiable. Not having too many services/helpers.
  • should the e-mail body be from a template, or does an heredoc looks sufficient to you?
  • which options would make sense for the command? for instance, redirecting or authenticating the user after having changed his password?
  • how to handle missing password setter/User class without an e-mail field? Display a warning in console?

Screenshots:

image

image

image

image

Thank you for this great bundle :)

@romaricdrigon romaricdrigon force-pushed the romaricdrigon:feat-forgotten-password branch 6 times, most recently from 593718a to 59805d7 Feb 1, 2019

@romaricdrigon

This comment has been minimized.

Copy link
Author

commented Feb 1, 2019

Views were added, and the POC if fully functional (manually tested).
From now on I will stop amending my first commit, if people want to review/comment code.
I'm considering making the "request password" form (first screenshot) work only by e-mail address, as recommended here.

@sstok

This comment has been minimized.

Copy link

commented Feb 1, 2019

I strongly suggest you read this 👍 https://paragonie.com/blog/2016/09/untangling-forget-me-knot-secure-account-recovery-made-simple

Almost all password reset systems are "horrible" insecure, tld;dr explanation: by using a single token (basically a random username) it's possible for an attack to guess the existing token by using a timing attack. When you provide a token, the database will use the index to look for an existing record, each character is checked against a binary-tree until it finds a record. When a character has a hit this means the time takes longer (because it continues the traversing of the tree), when there is no hit the time takes shorter because it bails out. Now using this technique an attack can use this information to try as many combinations as possible until all characters matches!

This is similar to a brute-force attack to guess a username/ID, but with a major difference. There is no password, and therefor access is directly granted.

By using the SplitToken you have selector (random username) and a verifier (random password in a hashed format), meaning that an attacker first needs to guess the selector, and then when there is a positive match, try to guess the verifier. BUT! Once you provide a wrong verifier for the selector, you simple revoke the token and no further tries are possible, even then guessing the verifier is nearly impossible as you compare the hashes in constant time (always takes the same amount of time) which leaks no timing information, making guessing nearly impossible.

I actually created a library that uses this technique https://github.com/rollerworks/split-token (MPLv2.0 licensed because it depends on HiddenString) but for most applications using the technique descried in the linked article is already a good step.

@romaricdrigon

This comment has been minimized.

Copy link
Author

commented Feb 1, 2019

@sstok thank you for the article, but I'm not sure implementing the suggestions from the article would be useful for most users The goal is to offer a basic feature, done right, for beginners. I'm pretty sure developers having tighter security requirements won't be using a maker command.
Hence I'm re-using a process from FOSUserBundle, which proved since a few years good enough for most, and follow some reasonable best practices (https://www.troyhunt.com/everything-you-ever-wanted-to-know/).

I like your approach, though. Did you consider make a "SplitTokenBundle"?

@gsylvestre

This comment has been minimized.

Copy link

commented Feb 7, 2019

This PR is a really good idea, and would be very usefull. I tried it and I love very much the generated code.

Some really small problems:

  • The generated twig files and folders do not follow coding stardards in their camelCased form (they should be snake_cased).
  • In the controller, method request(), is there any reason that you inject Twig_Environment instead of using the simpler approch of $this->renderView() ?
  • The suffix Action should be removed on controller's method checkEmailAction().

Also, I understand completely that you want to keep things simple and basic, and I agree. I just wonder if we could kinda incorporate some of @sstok suggestions by passing along the user email in the reset link. That would allow us to hash the reset token in the database, without adding much generated code and no other field in the entity. We even already use the UserPasswordEncoderInterface in the reset() method...

@gsylvestre

This comment has been minimized.

Copy link

commented Feb 7, 2019

In fact, maybe that those should be command options:

The reset password system need to store random tokens for your user. Do you want to store those:

  1. In your User entity
  2. In a separate entity

Would you like to hash the stored tokens (more secure)?

  1. Yes
  2. No

Would you like to add an extra "selector" field to protect against timed attack (more secure)?

  1. Yes
  2. No

Obviously, that is a lot of work to imlement, but that keep things simple for the end user.

@romaricdrigon

This comment has been minimized.

Copy link
Author

commented Feb 7, 2019

Thank you for the review @gsylvestre
I fixed the small problems, regarding \Twig_Environment at some point I wanted to render blocks independently, but I dropped this approach as it uses internal methods.

Regarding going further, I'm indecisive for now. I would like to get more feedbacks on this PR before going further regarding advanced features, about if and how it can integrates Maker bundle.

@sstok

This comment has been minimized.

Copy link

commented Feb 7, 2019

FYI. I don't have any plans for a bundle to integrate SplitToken, in practice you can register as many SplitToken factories as you want.

The only thing that might be interesting is a FormType to handle this format native, I actually did this for Park-Manager . But that's something for later as it's still experimental.

@weaverryan

This comment has been minimized.

Copy link
Member

commented Feb 25, 2019

Sorry for the delay. I really want this - and it's on my list to review - but haven't had time yet. Thanks for getting it started!

@weaverryan

This comment has been minimized.

Copy link
Member

commented Mar 1, 2019

@gsylvestre I was interested in getting more info from your above comments:

In fact, maybe that those should be command options:

The reset password system need to store random tokens for your user. Do you want to store those:

  1. In your User entity
  2. In a separate entity

I don't think we should do this - that's a level of customization that you can do if you want to, but I don't see any advantage to adding this flexibility (or is there a security implication?)

Would you like to hash the stored tokens (more secure)?

  1. Yes
  2. No

I saw your other comment before this, but can you explain this a bit more? If we only had 1 field in the database, how is the token hashed and how does that improve security?

Would you like to add an extra "selector" field to protect against timed attack (more secure)?

  1. Yes
  2. No

Maybe ;). I do think this is something to keep in mind as we get closer - it's probably not that much more work to implement, if indeed this is an industry-standard way of making more secure reset passwords.

@weaverryan

This comment has been minimized.

Copy link
Member

commented Mar 1, 2019

Oh, I understand now about the hashed tokens: if we passed the email address in the reset URL, then we could look up the User by that email address, and then do a timing-safe check to see if the hash in the URL matches what we expect in the database.

Of course, then this also in theory allows for a timing attack on that "reset" URL, because you could use it to detect timing differences in return to determine if the email you entered is an email in the system :).

In general, I think we should be looking at other popular implementations (including FOSUserBundle, Laravel, Rails, whatever) and making sure we have the same safeguards at least as they do.

@weaverryan
Copy link
Member

left a comment

Feedback started! I ran out of time to go through everything. But WOW! This is a crazy awesome and huge start! Let's work together and get this awesome command to the people :)

Show resolved Hide resolved src/Maker/MakeForgottenPassword.php Outdated
Show resolved Hide resolved src/Maker/MakeForgottenPassword.php Outdated
Show resolved Hide resolved src/Maker/MakeForgottenPassword.php
Show resolved Hide resolved src/Maker/MakeForgottenPassword.php Outdated
Show resolved Hide resolved src/Maker/MakeForgottenPassword.php Outdated
Show resolved Hide resolved ...Resources/skeleton/forgottenPassword/ForgottenPasswordController.tpl.php Outdated
Show resolved Hide resolved src/Maker/MakeForgottenPassword.php Outdated
Show resolved Hide resolved ...Resources/skeleton/forgottenPassword/ForgottenPasswordController.tpl.php Outdated
@gsylvestre

This comment has been minimized.

Copy link

commented Mar 2, 2019

@weaverryan yes that's what I meant: if we hash the token in the database, we then need some kind of identifier to retrieve it. As you say, if we use the email, then an attacker can use timed attacks to find out if it's in the db... Really not a big concern imo, but still.

I was not proposing the "separate entity" option for security implication, not directly. But now that I have looked around and thought about it, I believe that we should always store the token in a new entity, instead of inside properties in the user class. See below...

Something like :

class PasswordResetToken 
{
    $id
    $selector //(crypto secure string, shorter)
    $token //(crypto secure string, longer)
    $requestedAt //(datetime)
    $user //(OneToOne unidir relation)
}

Pros:

  • We do not mess with User class by adding new properties and methods. It is often already clogged, and I feel it is intrusive.
  • We can then more freely add more fields and methods to our Token entity, like the generated "selector", or ie. if the developper want, he/she can add an expireAt field easily, or maybe use this entity for email confirmation purposes.
  • Indirectly more secure because we can now add the "selector" field
  • Until a password reset request is made, no token are generated. And no fields are sitting there being NULL and sad in the user table.
  • Better model structure, added flexibility

Cons:

  • Extra entity and database table are generated
  • more complicated?

In fact, is it really more complicated for the developper to understand what is going on? I believe even beginners will get this new entity easily.
And we are doing a really secure password reset. Why settle for less? We expect Symfony to provide top secure features.

BTW, Laravel is storing reset tokens in a new table, but use the email as the selector.

I can implement these changes next week, if the coding work is a problem.

@romaricdrigon

This comment has been minimized.

Copy link
Author

commented Mar 2, 2019

Thank you for your inputs both of you, and for your review @weaverryan, that's great to see things moving. Let's make this maker happen!

@gsylvestre I like that last idea you had, having a separate entity. It is not making things overly complex, but it both adds some security from what I got & code is a little bit cleaner in my opinion (it prevents User entity from growing too big...). I can work on it while fixing the other comments, but I would appreciate your review.

@sstok

This comment has been minimized.

Copy link

commented Mar 4, 2019

Of course, then this also in theory allows for a timing attack on that "reset" URL, because you could use it to detect timing differences in return to determine if the email you entered is an email in the system :).

See the linked article for full details ;) https://paragonie.com/blog/2016/09/untangling-forget-me-knot-secure-account-recovery-made-simple

The main focus is that you have two factors for authenticating, a selector (like a username, email address or random token) and a verifier that acts like a password (with the same security requirements for verifying). Using only a random value (token) as-is (as the FOSUserBundle is doing) makes that your token is nothing more then a random id that can be guessed, I can't make it more clear than this 👍

@romaricdrigon

This comment has been minimized.

Copy link
Author

commented Mar 5, 2019

I pushed commits regarding the first review comments.
On top of that, I prefixed both form classes names by Password, I find PasswordRequestFormType and PasswordResettingFormType to be more explicit.

Now I will work on having a separate entity, and then on testing everything.

@stof

This comment has been minimized.

Copy link
Member

commented Mar 5, 2019

Note that if I were building FOSUserBundle today, I would probably use a separate table to store the reset tokens instead of putting things in the User class.

@sstok the issue of using the email address as the selector is that it allows using a timing attack to identify whether the email exists in the DB, allowing to leak the user base. That's why using a random selector instead is better if you try to protect the system against timing attacks.
Another option is to use the primary key of the user table...

@gsylvestre

This comment has been minimized.

Copy link

commented Mar 5, 2019

@stof, would you rather go with the random selector or the primary key?
I feel like the id would seem unsafe (for users and developpers to see the easily guessable user id in the url), even if it's mostly the same security level... It is one less field to generate on the other hand.

@romaricdrigon

This comment has been minimized.

Copy link
Author

commented Mar 7, 2019

I feel like having an extra "selector" field is not that bad, so for now I'm going with a selector separate from the ID.
One issue I'm having though: how to hash "token" without hardcoding a reference to the hash algorithm & without adding a dependency to an extra library?
For now, I'm considering getting the "UserPasswordEncoder", it feels a little bit hacky though.

@romaricdrigon romaricdrigon force-pushed the romaricdrigon:feat-forgotten-password branch 5 times, most recently from f8cffc9 to b2294a6 Mar 12, 2019

@romaricdrigon romaricdrigon force-pushed the romaricdrigon:feat-forgotten-password branch 4 times, most recently from 7cfdf5e to bdfd6ec Mar 13, 2019

@romaricdrigon

This comment has been minimized.

Copy link
Author

commented Mar 13, 2019

CI is green 🎉
I added half a dozen functional tests on major cases (password reset email is sent, wrong token, expired token...). I'm waiting to hear from you now now, ping @weaverryan :)

@bocharsky-bw
Copy link

left a comment

Great feature, I'm really looking forward to it!

I left some minor comments and my thoughts on it.

}

public function getUser(): <?= $user_class_name ?>

This comment has been minimized.

Copy link
@bocharsky-bw

This comment has been minimized.

Copy link
@romaricdrigon

romaricdrigon Apr 4, 2019

Author

You have such extra lines at many points in that file. The PasswordResetToken generated file is fine though, there may be an issue in the code generator & that's the way around I found. I couldn't quite dig into that, but find that hack.

<h1>Recover your password</h1>

{{ form_start(requestForm) }}
{{ form_row(requestForm.<?= $email_field ?>) }}

This comment has been minimized.

Copy link
@bocharsky-bw

bocharsky-bw Apr 4, 2019

I think we can just do {{ form_widget(requestForm) }} here for simplicity

This comment has been minimized.

Copy link
@romaricdrigon

romaricdrigon Apr 4, 2019

Author

I have mixed feeling there. Result will be the same, but it will be easier for developers customize the label with form_row. If we use form_widget, it may create a trap for newcomers who will modify the form label or the form theme.

This comment has been minimized.

Copy link
@bocharsky-bw

bocharsky-bw Apr 5, 2019

Ah, I see... I almost always do everything in form types. Anyway, it's minor and as far as it works the same - I'm fine with it 👍

Show resolved Hide resolved src/Resources/skeleton/forgottenPassword/twig_check_email.tpl.php Outdated

{% block body %}
<p>An email has been sent. It contains a link you must click to reset your password. This link will expires in {{ tokenLifetime }} hours.</p>
<p>If you don't get an email please check your spam folder or try again.</p>

This comment has been minimized.

Copy link
@bocharsky-bw

bocharsky-bw Apr 4, 2019

We can add "retry TTL" feature to prevent requesting password resetting too often. IIRC, FOSUserBundle has this feature. The idea is to do not send email again until RETRY_TTL ends - helps to save email bandwidth and avoid sending unnecessary emails. Also helps with spammers a bit. We can do it silently, saying that email was send, or show a clear message that user istrying to request password again too soon.

This comment has been minimized.

Copy link
@romaricdrigon

romaricdrigon Apr 4, 2019

Author

Doing it silently sounds good, otherwise the feature could be used to reveal an user exists for this e-mail address in the app. I will work on that.

This comment has been minimized.

Copy link
@bocharsky-bw

bocharsky-bw Apr 5, 2019

Awesome! Sounds good to me too 👍 Thanks!

<h1>Reset your password</h1>

{{ form_start(resetForm) }}
{{ form_row(resetForm.plainPassword) }}

This comment has been minimized.

Copy link
@bocharsky-bw

bocharsky-bw Apr 4, 2019

The same here, just {{ form_widget(resetForm) }} is enough I think, you don't need to think about fields ordering, fields name, etc.

This comment has been minimized.

Copy link
@romaricdrigon

romaricdrigon Apr 4, 2019

Author

Same point, then I may change both for consistency

Show resolved Hide resolved ...Resources/skeleton/forgottenPassword/ForgottenPasswordController.tpl.php
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;

This comment has been minimized.

Copy link
@bocharsky-bw

bocharsky-bw Apr 4, 2019

I would add @Route("/forgotten-password") to prefix all the routes here

This comment has been minimized.

Copy link
@romaricdrigon

romaricdrigon Apr 4, 2019

Author

Then the first would be @Route("/request", name="app_forgotten_password_request"), does it sound good?

This comment has been minimized.

Copy link
@bocharsky-bw

bocharsky-bw Apr 5, 2019

Perfect 👍 Well, we can leave @Route("/", name="app_forgotten_password") but /request is even better I think

*/
public function checkEmail()
{
return $this->render('forgottenPassword/check_email.html.twig', [

This comment has been minimized.

Copy link
@bocharsky-bw

bocharsky-bw Apr 4, 2019

This route is always available, so you open it even if you did not request password resetting. What about to check referer for it? Something like:

        // Redirect if user does not come from request reset password page
        $refererUrl = $request->headers->get('referer');
        $requestUrl = $this->generateUrl('app_forgotten_password');
        if (!$refererUrl) {
            return $this->redirect($requestUrl);
        }
        // It's assumed we didn't have any query params on the Request page,
        // otherwise we need a more clever solution here like using parse_url()
        if ($requestUrl !== substr($refererUrl, -strlen($requestUrl))) {
            return $this->redirect($requestUrl);
        }

       return $this->render(...)

So, only users who were redirected from app_forgotten_password would be able to see this page

This comment has been minimized.

Copy link
@romaricdrigon

romaricdrigon Apr 4, 2019

Author

A referer check is pretty weak. What about adding some extra (random) token both in User session & as an attribute to checkEmail route?

This comment has been minimized.

Copy link
@bocharsky-bw

bocharsky-bw Apr 5, 2019

Sounds cool, I like it 👍 So, when "check email" page is rendered - we need to invalidate that token (remove it), right? And if the token is invalid - redirect to app_forgotten_password

This comment has been minimized.

Copy link
@romaricdrigon

romaricdrigon Apr 5, 2019

Author

Exactly!

Show resolved Hide resolved ...Resources/skeleton/forgottenPassword/ForgottenPasswordController.tpl.php Outdated
Show resolved Hide resolved ...Resources/skeleton/forgottenPassword/ForgottenPasswordController.tpl.php

@romaricdrigon romaricdrigon force-pushed the romaricdrigon:feat-forgotten-password branch from bdfd6ec to c549447 Apr 4, 2019

@romaricdrigon

This comment has been minimized.

Copy link
Author

commented Apr 4, 2019

Thank you @bocharsky-bw for the review! I addressed most of your comments, and I would like to discuss some more on some.

@romaricdrigon romaricdrigon force-pushed the romaricdrigon:feat-forgotten-password branch from c549447 to ee1e938 Apr 4, 2019

@bocharsky-bw
Copy link

left a comment

One more thing: When I try again to request password resetting but I already requested it - I get 500 error due to:

Integrity constraint violation: 1062 Duplicate entry for user ID

Do we have a solution for it?

@romaricdrigon

This comment has been minimized.

Copy link
Author

commented Apr 5, 2019

Hmm, that's problematic indeed. Definitely we should have no limits there, because a user can request password several times (well, within discussed "retry TTL" limit). And there's no way not to leak any account information with such a feature. So I will modify the relationship to ManytoOne, and consider a test covering that scenario.

@romaricdrigon romaricdrigon force-pushed the romaricdrigon:feat-forgotten-password branch from ee1e938 to 0a8c397 Apr 5, 2019

@bocharsky-bw

This comment has been minimized.

Copy link

commented Apr 5, 2019

Yeah, looks like so... sounds good tome, but probably we need ManyToOne in this case, but probably you meant it 👍

@romaricdrigon

This comment has been minimized.

Copy link
Author

commented Apr 5, 2019

Exactly, I pushed the modification ;)

@romaricdrigon romaricdrigon force-pushed the romaricdrigon:feat-forgotten-password branch from 0a8c397 to 160929e Apr 5, 2019

@bocharsky-bw
Copy link

left a comment

Thanks for doing more tweaks here! Except a few minor comments in my previous review for what would be good to hear feedback from other devs, I would really like to see the next features in this PR. So, let me recap important things that are left here:

  • 1. use @Route("/forgotten-password") for the controller class (from #359 (comment) )
  • 2. use RepeatedType for password reset form (from #359 (comment) )
  • 3. make sure users are allowed to be on app_check_email, otherwise redirect them to app_forgotten_password (from #359 (comment) )
  • 4. retry TTL feature (from #359 (comment) )
  • 5. store the token in session and hide it from the URL feature (this one is the new and the last one from me I think, I described below in the review: #359 (comment) )

Any feedback from others is welcome!

}

/**
* @Route("/reset-password/{tokenAndSelector}", name="app_reset_password")

This comment has been minimized.

Copy link
@bocharsky-bw

bocharsky-bw Apr 7, 2019

And the last but not least important feature I would like to see in this PR - store token in session and hide it from the URL. It means we should make tokenAndSelector optional, and if the tokenAndSelector is passed - we store it in session and redirect users to just /reset-password/, but if there's no token in the URL - we read it from the session. If there's no token in both places - createNotFoundException() then. Wdyt?

This is a pretty important feature btw, because some JS scripts may reveal token to other third-party services when sending AJAX requests from this page, so this is important for security reasons.

@romaricdrigon

This comment has been minimized.

Copy link
Author

commented Apr 8, 2019

Thanks for the recap, I will try to work on those next week.
Once those are done, are we getting closer to a merge? This PR has been going for a while already, with quite some time of "sleep".

@bocharsky-bw

This comment has been minimized.

Copy link

commented Apr 8, 2019

I bet yes! @weaverryan is really want to merge this - just busy enough right now! Well, at least if we're talking about the "Forgotten password" feature in general, but probably he would request more minor code tweaks after all these new features, but it's going to be merged anyway.

@weaverryan

This comment has been minimized.

Copy link
Member

commented Apr 8, 2019

Yes, Victor has been keeping me up to date :). I should have time to play with this later in the week!

@stof

This comment has been minimized.

Copy link
Member

commented Apr 18, 2019

I'm wondering whether this should actually be implemented as a maker generating the whole feature in the project. If we ever find a flaw in the generated code, we will have no way to ship a patched code to all the projects which used the maker before the fix. At least I don't see a way for that.

@weaverryan

This comment has been minimized.

Copy link
Member

commented Apr 18, 2019

I’ve had that same thought... and of course you can try to think “does this contain sensitive code paths”, but trying to predict security flaws is of course impossible.

I had discussed this with @bocharsky-be originally as a bundle. We decided generation was better for DX - needing to override templates and configure stuff vs just changing the generated code is much harder.

Anyways, some more opinions on this would be great.

@stof

This comment has been minimized.

Copy link
Member

commented Apr 18, 2019

Well, maybe the email template itself should be configurable (with a maker to generate one you can customize). But I'm not sure other stuff needs to be overwritten (or even should be)

@romaricdrigon

This comment has been minimized.

Copy link
Author

commented Apr 18, 2019

As a developer, I actually prefer generated code versus a bundle. I prefer to deal with code rather than with configuration.
In a few years (2,3?) FOSUserBundle password reset feature did not change at all. I have the impression changes are unlikely. And in case we happen to have a fix to ship to all generated code, I wonder if something like an "updater" would be doable, given that most developers won't rename anything.

@bocharsky-bw

This comment has been minimized.

Copy link

commented Apr 19, 2019

"Updater" sounds interesting, then along with git diff it would be possible to review changes. But how to know that you have to run the updater in case you don't watch changes in MakerBundle?

@romaricdrigon

This comment has been minimized.

Copy link
Author

commented Apr 19, 2019

That raises a good question: if we discover a security issue in code generated by MakerBundle, I guess we should open a CVE for MakerBundle? So security-advisory could warn users if they did not update.
For the case where they update regularly, but a symfony.com blogpost, I don't quite see any way to warn them.

@stof

This comment has been minimized.

Copy link
Member

commented Apr 19, 2019

@romaricdrigon no, that would not work. Updating MakerBundle will not fix the vulnerability in their own project if they already generated the code before.

A vulnerability in the generated code would actually require a CVE for MakerBundle (so that projects stop generating insecure code) and a CVE for each project containing the existing insecure code. That's not something we can do for the second part. And that's why I think we should be careful about bring this whole password-recovery feature as a maker.

@romaricdrigon

This comment has been minimized.

Copy link
Author

commented May 7, 2019

@stof agreed, but how many times did we have such a vulnerability in FOSUserBundle reset password feature? My point is not that it is impossible, but that it is unlikely, plus the current code is already to some pretty high standards compared to what developers would do on their own. That's a (relative) progress?

I'm back from holidays, should I resume working on that?
It is not clear to me whether the features is OK with everybody or if it is on hold.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.