Skip to content

Commit

Permalink
Add a facility to set an SVN Password (#280)
Browse files Browse the repository at this point in the history
* Initial run at adding a SVN Password functionality.

* Linting and alerts

* Add some TODOs

* Remove the custom code to check for plugin committer / theme author status, and use the user_meta instead.

* Use WordPress.org provided functions to set / check for a password in use.

* typo

* Linting fixes.

* Remove nested terninaries

* Switch to a dedicated rest api endpoint to generate the SVN password.

* Require 2FA validation to enter the SVN password screen.

* Linting.

* Switch to 'Generate Password' rather than Request.

* JSX

* Simplify description of the Account Overview SVN password tab.

* Remove "forget password", these passwords are random and cannot be remembered.

* The SVN password is required to commit

* Switch to using the Copy to clipboard component.

* Temporarily limit access to the SVN Password interface to beta users only, until the systems side is finalised.

* Linting: remove unused function.

* Add some verbose about subversion text.

* Expand upon the text, combine paragraphs, list the case-sensitive username.

* When the SVN password functions aren't available, just return that the user doesn't have a password set, and an explanation instead of a SVN password.

* Updated interface design

* Add a case-sensitive remark when a username is not all lowercase.

* Use a list, add spacing.

* Convert the Copy-to-clipboard button into a link.

* Add the Generation date

* Fill in the creation date to avoid a lag in the UI updating.

* Update the account overview text.

* Add action container to the svn button to match other views.

* Update copy for svn blurb.

* Add copy intro class to maintain same spacing with other views

* Harmonize security keys and svn password detail styles.

* Add more contextual language to submit button.

* Update copy to SVN Credentials.

* Change props for CopyToClipboard button since it's reused here.

* Fix linter issue.

* Lowercase status text. That's the new standard.

* Enable the UI for all SVN-related users.

---------

Co-authored-by: StevenDufresne <dufresnesteven@gmail.com>
  • Loading branch information
dd32 and StevenDufresne authored Aug 29, 2024
1 parent 49f063a commit 19d8f88
Show file tree
Hide file tree
Showing 13 changed files with 2,696 additions and 2,795 deletions.
2 changes: 1 addition & 1 deletion settings/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"@wordpress/core-data": "^6.4.0",
"@wordpress/data": "^8.4.0",
"@wordpress/element": "^5.4.0",
"@wordpress/icons": "^9.18.0",
"@wordpress/icons": "^9.49.0",
"@wordpress/scripts": "^27.6.0",
"lodash": "^4.17.21"
}
Expand Down
89 changes: 89 additions & 0 deletions settings/rest-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
use Two_Factor_Core, Two_Factor_Totp, Two_Factor_Backup_Codes;
use WildWolf\WordPress\TwoFactorWebAuthn\{ WebAuthn_Credential_Store };
use WP_REST_Server, WP_REST_Request, WP_Error, WP_User;
use function WordPressdotorg\Security\SVNPasswords\{ set_svn_password, get_svn_password_creation_date };

defined( 'WPINC' ) || die();

Expand Down Expand Up @@ -131,6 +132,39 @@ function register_rest_routes() : void {
),
),
);

register_rest_route(
'wporg-two-factor/1.0',
'/generate-svn-password',
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => function( $request ) {
$user = get_userdata( $request['user_id'] );

// Local environment doesn't have the SVN password system, just mock it.
if ( ! function_exists( 'WordPressdotorg\Security\SVNPasswords\set_svn_password' ) ) {
return 'Local Development: SVN Password system unavailable.';
}

return [
'svn_password' => set_svn_password( $user->ID )
];
},
'permission_callback' => function( $request ) {
return Two_Factor_Core::rest_api_can_edit_user_and_update_two_factor_options( $request['user_id'] );
},
'args' => array(
'user_id' => array(
'required' => true,
'type' => 'number',
'sanitize_callback' => 'absint',
'validate_callback' => function( $user_id ) {
return get_userdata( $user_id ) instanceof WP_User;
},
),
),
),
);
}

/**
Expand Down Expand Up @@ -323,6 +357,61 @@ function register_user_fields(): void {
]
);

register_rest_field(
'user',
'svn_password_required',
[
'get_callback' => function( $user ) {
global $wpdb;

$user = get_userdata( $user['id'] );
if ( ! $user ) {
return false;
}

// Committers, supes, etc. It's likely these users will need a SVN password.
if ( function_exists( 'is_special_user' ) && is_special_user( $user->ID ) ) {
return true;
}

// Plugin committers & Theme authors have this user meta set.
if ( $user->has_plugins || $user->has_themes ) {
return true;
}

return false;
},
'schema' => [
'type' => 'boolean',
'context' => [ 'edit' ],
]
]
);

register_rest_field(
'user',
'svn_password_created',
[
'get_callback' => function( $user ) {
// Local environment doesn't have the SVN password system, just return false for that.
if ( ! function_exists( 'WordPressdotorg\Security\SVNPasswords\get_svn_password_creation_date' ) ) {
return false;
}

$svn_password_created_date = get_svn_password_creation_date( $user['id'] );
if ( ! $svn_password_created_date ) {
return false;
}

return $svn_password_created_date;
},
'schema' => [
'type' => [ 'boolean', 'string' ],
'context' => [ 'edit' ],
]
]
);

}

/**
Expand Down
24 changes: 23 additions & 1 deletion settings/src/components/account-status.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ export default function AccountStatus() {
const {
user: {
userRecord: {
record: { email, pending_email: pendingEmail },
record: {
email,
pending_email: pendingEmail,
svn_password_created: svnPasswordSet,
svn_password_required: svnPasswordRequired,
},
},
hasPrimaryProvider,
primaryProvider,
Expand Down Expand Up @@ -89,6 +94,23 @@ export default function AccountStatus() {
bodyText={ backupBodyText }
disabled={ ! hasPrimaryProvider }
/>

{ svnPasswordRequired || svnPasswordSet ? (
<SettingStatusCard
screen="svn-password"
status={
! svnPasswordRequired && ! svnPasswordSet ? 'info' : !! svnPasswordSet
}
headerText="SVN credentials"
bodyText={
! svnPasswordSet
? 'You have not configured a SVN password for your account.'
: "You've got a SVN password configured for your account."
}
/>
) : (
''
) }
</div>
);
}
Expand Down
2 changes: 1 addition & 1 deletion settings/src/components/backup-codes.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ function Setup( { setRegenerating, onSuccess } ) {
<CodeList codes={ backupCodes } />

<ButtonGroup>
<CopyToClipboardButton codes={ backupCodes } />
<CopyToClipboardButton contents={ backupCodes } />
<PrintButton />
<DownloadButton codes={ backupCodes } />
</ButtonGroup>
Expand Down
8 changes: 4 additions & 4 deletions settings/src/components/copy-to-clipboard-button.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@
import { useCallback, useState } from '@wordpress/element';
import { Button } from '@wordpress/components';

export default function CopyToClipboardButton( { codes } ) {
export default function CopyToClipboardButton( { contents, variant = 'secondary' } ) {
const [ copied, setCopied ] = useState( false );

const onClick = useCallback( () => {
navigator.clipboard.writeText( codes ).then( () => {
navigator.clipboard.writeText( contents ).then( () => {
setCopied( true );
setTimeout( () => setCopied( false ), 2000 );
} );
}, [ codes ] );
}, [ contents ] );

return (
<Button variant="secondary" onClick={ onClick }>
<Button variant={ variant } onClick={ onClick }>
{ copied ? 'Copied!' : 'Copy' }
</Button>
);
Expand Down
5 changes: 5 additions & 0 deletions settings/src/components/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import EmailAddress from './email-address';
import TOTP from './totp';
import WebAuthn from './webauthn/webauthn';
import BackupCodes from './backup-codes';
import SVNPassword from './svn-password';

import { GlobalContext } from '../script';

Expand Down Expand Up @@ -62,6 +63,10 @@ export default function Settings() {
/>
),
},
'svn-password': {
title: 'SVN credentials',
component: <SVNPassword />,
},
};

const currentScreenComponent =
Expand Down
127 changes: 127 additions & 0 deletions settings/src/components/svn-password.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/**
* WordPress dependencies
*/
import apiFetch from '@wordpress/api-fetch';
import { Button } from '@wordpress/components';
import { useCallback, useContext, useMemo, useState } from '@wordpress/element';
import { refreshRecord } from '../utilities/common';
import CopyToClipboardButton from './copy-to-clipboard-button';

/**
* Internal dependencies
*/
import { GlobalContext } from '../script';

/**
* Render the Email setting.
*/
export default function SVNPassword() {
const {
user: { userRecord },
setError,
} = useContext( GlobalContext );

const [ isGenerating, setGenerating ] = useState( false );
const [ generatedPassword, setGeneratedPassword ] = useState( '' );

// Generate a new SVN Password.
const handleGenerate = useCallback( async () => {
try {
setGenerating( true );

const response = await apiFetch( {
path: '/wporg-two-factor/1.0/generate-svn-password',
method: 'POST',
data: {
user_id: userRecord.record.id,
},
} );

// Fill in the creation date in the user record, we'll refresh it for the actual data below.
userRecord.record.svn_password_created = new Date().toISOString();

setGeneratedPassword( response.svn_password );
setGenerating( false );

await refreshRecord( userRecord );
} catch ( apiFetchError ) {
setError( apiFetchError );
}
} );

const getButtonText = useMemo( () => {
if ( isGenerating ) {
return 'Generating...';
}

if ( ! userRecord.record.svn_password_created ) {
return 'Generate Password';
}

return 'Regenerate Password';
}, [ isGenerating, userRecord.record.svn_password_created ] );

return (
<>
<p>
WordPress.org uses Subversion (SVN) for version control, providing each hosted
plugin and theme with a repository that the author can commit to. For information on
using SVN, please see the{ ' ' }
<a href="https://developer.wordpress.org/plugins/wordpress-org/how-to-use-subversion/">
WordPress.org Plugin Developer Handbook
</a>
.
</p>

<p className="wporg-2fa__screen-intro">
For security, your WordPress.org account password should not be used to commit to
SVN, use a separate SVN password, which you can generate here.
</p>

<h4>Details</h4>
<ul>
<li>
Username: <code>{ userRecord.record.username }</code>{ ' ' }
{ userRecord.record.username.match( /[^a-z0-9]/ ) && <>(case-sensitive)</> }
</li>
<li>
Password:{ ' ' }
{ generatedPassword || userRecord.record.svn_password_created ? (
<>
<code>{ generatedPassword || 'svn_*****************' }</code>
&nbsp;
{ generatedPassword && (
<CopyToClipboardButton
variant="link"
contents={ generatedPassword }
/>
) }
{ userRecord.record.svn_password_created && (
<div className="wporg-2fa__svn-password_generated">
Generated on{ ' ' }
{ new Date(
userRecord.record.svn_password_created
).toLocaleDateString() }
</div>
) }
</>
) : (
<>
<em>Not configured</em>
</>
) }
</li>
</ul>
<div className="wporg-2fa__submit-actions">
<Button
variant="secondary"
onClick={ handleGenerate }
isBusy={ isGenerating }
disabled={ isGenerating }
>
{ getButtonText }
</Button>
</div>
</>
);
}
26 changes: 26 additions & 0 deletions settings/src/components/svn-password.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
.wporg-2fa__svn-password {
code {
display: inline-block;
padding: 0 3px;
background: var(--wp--preset--color--light-grey-2, #f6f6f6);
border-radius: 2px;
}

ul {
padding-top: 16px;

li:not(:last-child) {
margin-bottom: 8px;

.wporg-2fa__svn-password_generated {
color: var(--wp--preset--color--charcoal-4, #656a71);
}
}
}

.wporg-2fa__svn-password_generated {
padding-top: 4px;
font-size: 12px;
color: var(--wp--preset--color--charcoal-4, #656a71);
}
}
2 changes: 1 addition & 1 deletion settings/src/components/webauthn/list-keys.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export default function ListKeys() {

return (
<>
<h4 className="wporg-2fa__webauthn-keys-header">Security Keys</h4>
<h4>Security Keys</h4>
<ul className="wporg-2fa__webauthn-keys-list">
{ keys.map( ( key ) => (
<li key={ key.id }>
Expand Down
5 changes: 0 additions & 5 deletions settings/src/components/webauthn/webauthn.scss
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
.wporg-2fa__webauthn,
.bbp-single-user .wporg-2fa__webauthn {
.wporg-2fa__webauthn-keys-header {
margin-bottom: 0;
font-weight: 600;
}

.wporg-2fa__webauthn-keys-list {
li {
display: flex;
Expand Down
2 changes: 1 addition & 1 deletion settings/src/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ function Main( { userId, isOnboarding } ) {
const [ screen, setScreen ] = useState( initialScreen === null ? 'home' : initialScreen );

// The screens where a recent two factor challenge is required.
const twoFactorRequiredScreens = [ 'webauthn', 'totp', 'backup-codes' ];
const twoFactorRequiredScreens = [ 'webauthn', 'totp', 'backup-codes', 'svn-password' ];

// Listen for back/forward button clicks.
useEffect( () => {
Expand Down
Loading

0 comments on commit 19d8f88

Please sign in to comment.