diff --git a/lib/class-wp-rest-blocks-controller.php b/lib/class-wp-rest-blocks-controller.php
index 2b19e2a5f0b0e..1e656641c7395 100644
--- a/lib/class-wp-rest-blocks-controller.php
+++ b/lib/class-wp-rest-blocks-controller.php
@@ -16,6 +16,39 @@
  * @see WP_REST_Controller
  */
 class WP_REST_Blocks_Controller extends WP_REST_Posts_Controller {
+	/**
+	 * Checks if a block can be read.
+	 *
+	 * @since 2.1.0
+	 *
+	 * @param object $post Post object that backs the block.
+	 * @return bool Whether the block can be read.
+	 */
+	public function check_read_permission( $post ) {
+		// Ensure that the user is logged in and has the read_blocks capability.
+		$post_type = get_post_type_object( $post->post_type );
+		if ( ! current_user_can( $post_type->cap->read_post, $post->ID ) ) {
+			return false;
+		}
+
+		return parent::check_read_permission( $post );
+	}
+
+	/**
+	 * Handle a DELETE request.
+	 *
+	 * @since 1.10.0
+	 *
+	 * @param WP_REST_Request $request Full details about the request.
+	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
+	 */
+	public function delete_item( $request ) {
+		// Always hard-delete a block.
+		$request->set_param( 'force', true );
+
+		return parent::delete_item( $request );
+	}
+
 	/**
 	 * Given an update or create request, build the post object that is saved to
 	 * the database.
@@ -25,33 +58,22 @@ class WP_REST_Blocks_Controller extends WP_REST_Posts_Controller {
 	 * @param WP_REST_Request $request Request object.
 	 * @return stdClass|WP_Error Post object or WP_Error.
 	 */
-	protected function prepare_item_for_database( $request ) {
-		$prepared_post = new stdClass;
-
-		if ( isset( $request['id'] ) ) {
-			$existing_post = $this->get_post( $request['id'] );
-			if ( is_wp_error( $existing_post ) ) {
-				return $existing_post;
-			}
-
-			$prepared_post->ID = $existing_post->ID;
-		}
+	public function prepare_item_for_database( $request ) {
+		$prepared_post = parent::prepare_item_for_database( $request );
 
-		$prepared_post->post_title   = $request['title'];
-		$prepared_post->post_content = $request['content'];
-		$prepared_post->post_type    = $this->post_type;
-		$prepared_post->post_status  = 'publish';
+		// Force blocks to always be published.
+		$prepared_post->post_status = 'publish';
 
-		return apply_filters( "rest_pre_insert_{$this->post_type}", $prepared_post, $request );
+		return $prepared_post;
 	}
 
 	/**
-	 * Given a post from the database, build the array that is returned from an
+	 * Given a block from the database, build the array that is returned from an
 	 * API response.
 	 *
 	 * @since 1.10.0
 	 *
-	 * @param WP_Post         $post    Post object.
+	 * @param WP_Post         $post    Post object that backs the block.
 	 * @param WP_REST_Request $request Request object.
 	 * @return WP_REST_Response Response object.
 	 */
@@ -67,21 +89,6 @@ public function prepare_item_for_response( $post, $request ) {
 		return apply_filters( "rest_prepare_{$this->post_type}", $response, $post, $request );
 	}
 
-	/**
-	 * Handle a DELETE request.
-	 *
-	 * @since 1.10.0
-	 *
-	 * @param WP_REST_Request $request Full details about the request.
-	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
-	 */
-	public function delete_item( $request ) {
-		// Always hard-delete a block.
-		$request->set_param( 'force', true );
-
-		return parent::delete_item( $request );
-	}
-
 	/**
 	 * Builds the block's schema, conforming to JSON Schema.
 	 *
diff --git a/lib/register.php b/lib/register.php
index 56f0d809d9b9f..209d47b3810ca 100644
--- a/lib/register.php
+++ b/lib/register.php
@@ -401,11 +401,48 @@ function gutenberg_register_post_types() {
 			'singular_name' => 'Block',
 		),
 		'public'                => false,
-		'capability_type'       => 'post',
 		'show_in_rest'          => true,
 		'rest_base'             => 'blocks',
 		'rest_controller_class' => 'WP_REST_Blocks_Controller',
+		'capability_type'       => 'block',
+		'capabilities'          => array(
+			'read'         => 'read_blocks',
+			'create_posts' => 'create_blocks',
+		),
+		'map_meta_cap'          => true,
 	) );
+
+	foreach ( array( 'administrator', 'editor' ) as $role_name ) {
+		$editor = get_role( $role_name );
+		$editor->add_cap( 'edit_blocks' );
+		$editor->add_cap( 'edit_others_blocks' );
+		$editor->add_cap( 'publish_blocks' );
+		$editor->add_cap( 'read_private_blocks' );
+		$editor->add_cap( 'read_blocks' );
+		$editor->add_cap( 'delete_blocks' );
+		$editor->add_cap( 'delete_private_blocks' );
+		$editor->add_cap( 'delete_published_blocks' );
+		$editor->add_cap( 'delete_others_blocks' );
+		$editor->add_cap( 'edit_private_blocks' );
+		$editor->add_cap( 'edit_published_blocks' );
+		$editor->add_cap( 'create_blocks' );
+	}
+
+	$author = get_role( 'author' );
+	$author->add_cap( 'edit_blocks' );
+	$author->add_cap( 'publish_blocks' );
+	$author->add_cap( 'read_blocks' );
+	$author->add_cap( 'delete_blocks' );
+	$author->add_cap( 'delete_published_blocks' );
+	$author->add_cap( 'edit_published_blocks' );
+	$author->add_cap( 'create_blocks' );
+
+	$contributor = get_role( 'contributor' );
+	$contributor->add_cap( 'edit_blocks' );
+	$contributor->add_cap( 'read_blocks' );
+	$contributor->add_cap( 'delete_blocks' );
+	$contributor->add_cap( 'delete_published_blocks' );
+	$contributor->add_cap( 'edit_published_blocks' );
 }
 add_action( 'init', 'gutenberg_register_post_types' );
 
diff --git a/phpunit/class-rest-blocks-controller-test.php b/phpunit/class-rest-blocks-controller-test.php
index d45071adb39f6..3fa19b7390562 100644
--- a/phpunit/class-rest-blocks-controller-test.php
+++ b/phpunit/class-rest-blocks-controller-test.php
@@ -197,6 +197,128 @@ public function test_get_item_schema() {
 		$this->assertArrayHasKey( 'content', $properties );
 	}
 
+	/**
+	 * Test cases for test_capabilities().
+	 */
+	public function data_capabilities() {
+		return array(
+			array( 'create', 'editor', 201 ),
+			array( 'create', 'author', 201 ),
+			array( 'create', 'contributor', 403 ),
+			array( 'create', null, 401 ),
+
+			array( 'read', 'editor', 200 ),
+			array( 'read', 'author', 200 ),
+			array( 'read', 'contributor', 200 ),
+			array( 'read', null, 401 ),
+
+			array( 'update_delete_own', 'editor', 200 ),
+			array( 'update_delete_own', 'author', 200 ),
+			array( 'update_delete_own', 'contributor', 200 ),
+
+			array( 'update_delete_others', 'editor', 200 ),
+			array( 'update_delete_others', 'author', 403 ),
+			array( 'update_delete_others', 'contributor', 403 ),
+			array( 'update_delete_others', null, 401 ),
+		);
+	}
+
+	/**
+	 * Exhaustively check that each role either can or cannot create, edit,
+	 * update, and delete reusable blocks.
+	 *
+	 * @dataProvider data_capabilities
+	 */
+	public function test_capabilities( $action, $role, $expected_status ) {
+		if ( $role ) {
+			$user_id = $this->factory->user->create( array( 'role' => $role ) );
+			wp_set_current_user( $user_id );
+		} else {
+			wp_set_current_user( 0 );
+		}
+
+		switch ( $action ) {
+			case 'create':
+				$request = new WP_REST_Request( 'POST', '/wp/v2/blocks' );
+				$request->set_body_params(
+					array(
+						'title'   => 'Test',
+						'content' => '<!-- wp:core/paragraph --><p>Test</p><!-- /wp:core/paragraph -->',
+					)
+				);
+
+				$response = $this->server->dispatch( $request );
+				$this->assertEquals( $expected_status, $response->get_status() );
+
+				break;
+
+			case 'read':
+				$request = new WP_REST_Request( 'GET', '/wp/v2/blocks/' . self::$post_id );
+
+				$response = $this->server->dispatch( $request );
+				$this->assertEquals( $expected_status, $response->get_status() );
+
+				break;
+
+			case 'update_delete_own':
+				$post_id = wp_insert_post(
+					array(
+						'post_type'    => 'wp_block',
+						'post_status'  => 'publish',
+						'post_title'   => 'My cool block',
+						'post_content' => '<!-- wp:core/paragraph --><p>Hello!</p><!-- /wp:core/paragraph -->',
+						'post_author'  => $user_id,
+					)
+				);
+
+				$request = new WP_REST_Request( 'PUT', '/wp/v2/blocks/' . $post_id );
+				$request->set_body_params(
+					array(
+						'title'   => 'Test',
+						'content' => '<!-- wp:core/paragraph --><p>Test</p><!-- /wp:core/paragraph -->',
+					)
+				);
+
+				$response = $this->server->dispatch( $request );
+				$this->assertEquals( $expected_status, $response->get_status() );
+
+				$request = new WP_REST_Request( 'DELETE', '/wp/v2/blocks/' . $post_id );
+
+				$response = $this->server->dispatch( $request );
+				$this->assertEquals( $expected_status, $response->get_status() );
+
+				wp_delete_post( $post_id );
+
+				break;
+
+			case 'update_delete_others':
+				$request = new WP_REST_Request( 'PUT', '/wp/v2/blocks/' . self::$post_id );
+				$request->set_body_params(
+					array(
+						'title'   => 'Test',
+						'content' => '<!-- wp:core/paragraph --><p>Test</p><!-- /wp:core/paragraph -->',
+					)
+				);
+
+				$response = $this->server->dispatch( $request );
+				$this->assertEquals( $expected_status, $response->get_status() );
+
+				$request = new WP_REST_Request( 'DELETE', '/wp/v2/blocks/' . self::$post_id );
+
+				$response = $this->server->dispatch( $request );
+				$this->assertEquals( $expected_status, $response->get_status() );
+
+				break;
+
+			default:
+				$this->fail( "'$action' is not a valid action." );
+		}
+
+		if ( isset( $user_id ) ) {
+			self::delete_user( $user_id );
+		}
+	}
+
 	public function test_context_param() {
 		$this->markTestSkipped( 'Controller doesn\'t implement get_context_param().' );
 	}