Berechtigungsgetriebene Entwicklung

Eine Präsentation von @felixarntz

Definitionen

Berechtigungen (capabilities) in WordPress beschreiben Aktionen, die ein bestimmter Benutzer durchführen oder nicht durchführen darf.

Rollen (roles) in WordPress sind dafür verantwortlich festzulegen, über welche Berechtigungen ein Benutzer verfügt oder nicht verfügt.

WordPress-Berechtigungen

  • read
  • edit_posts
  • upload_files
  • manage_categories
  • manage_options
  • ...

Eigene Berechtigungen

  • read_ct_tutorials
  • edit_ct_tutorials
  • manage_ct_options
  • ...

Warum auf Berechtigungen achten?

  • Security: Niemand hat Zugriff, der keinen Zugriff haben soll.
  • Usability: Nur die Bereiche, die ein Benutzer tatsächlich braucht, werden angezeigt.
  • Customizability: Andere Entwickler können den Zugriff auf deine Funktionalität anpassen.

Intern: Die options-Tabelle

Die verfügbaren Rollen für eine Website sind in der options-Datenbanktabelle als serialisiertes Array gespeichert.

Die Berechtigungen, die eine Rolle gewährt, sind ebenfalls Teil dieses Arrays.

array(
	'administrator' => array(
		'name'         => 'Administrator',
		'capabilities' => array(
			'switch_themes'          => true,
			'edit_themes'            => true,
			'activate_plugins'       => true,
			'edit_plugins'           => true,
			'edit_users'             => true,
			'edit_files'             => true,
			'manage_options'         => true,
			'moderate_comments'      => true,
			'manage_categories'      => true,
			'manage_links'           => true,
			'upload_files'           => true,
			'import'                 => true,
			'unfiltered_html'        => true,
			'edit_posts'             => true,
			'edit_others_posts'      => true,
			'edit_published_posts'   => true,
			'publish_posts'          => true,
			'edit_pages'             => true,
			'read'                   => true,
			'edit_others_pages'      => true,
			'edit_published_pages'   => true,
			'publish_pages'          => true,
			'delete_pages'           => true,
			'delete_others_pages'    => true,
			'delete_published_pages' => true,
			'delete_posts'           => true,
			'delete_others_posts'    => true,
			'delete_published_posts' => true,
			'delete_private_posts'   => true,
			'edit_private_posts'     => true,
			'read_private_posts'     => true,
			'delete_private_pages'   => true,
			'edit_private_pages'     => true,
			'read_private_pages'     => true,
			'delete_users'           => true,
			'create_users'           => true,
			'unfiltered_upload'      => true,
			'edit_dashboard'         => true,
			'update_plugins'         => true,
			'delete_plugins'         => true,
			'install_plugins'        => true,
			'update_themes'          => true,
			'install_themes'         => true,
			'update_core'            => true,
			'list_users'             => true,
			'remove_users'           => true,
			'promote_users'          => true,
			'edit_theme_options'     => true,
			'delete_themes'          => true,
			'export'                 => true,
		),
	),
	'editor'        => array(
		'name'         => 'Editor',
		'capabilities' => array(
			'moderate_comments'      => true,
			'manage_categories'      => true,
			'manage_links'           => true,
			'upload_files'           => true,
			'unfiltered_html'        => true,
			'edit_posts'             => true,
			'edit_others_posts'      => true,
			'edit_published_posts'   => true,
			'publish_posts'          => true,
			'edit_pages'             => true,
			'read'                   => true,
			'edit_others_pages'      => true,
			'edit_published_pages'   => true,
			'publish_pages'          => true,
			'delete_pages'           => true,
			'delete_others_pages'    => true,
			'delete_published_pages' => true,
			'delete_posts'           => true,
			'delete_others_posts'    => true,
			'delete_published_posts' => true,
			'delete_private_posts'   => true,
			'edit_private_posts'     => true,
			'read_private_posts'     => true,
			'delete_private_pages'   => true,
			'edit_private_pages'     => true,
			'read_private_pages'     => true,
		),
	),
	'author'        => array(
		'name'         => 'Author',
		'capabilities' => array(
			'upload_files'           => true,
			'edit_posts'             => true,
			'edit_published_posts'   => true,
			'publish_posts'          => true,
			'read'                   => true,
			'delete_posts'           => true,
			'delete_published_posts' => true,
		),
	),
	'contributor'   => array(
		'name'         => 'Contributor',
		'capabilities' => array(
			'edit_posts'   => true,
			'read'         => true,
			'delete_posts' => true,
		),
	),
	'subscriber'    => array(
		'name'         => 'Subscriber',
		'capabilities' => array(
			'read'    => true,
		),
	),
);

Intern: Die usermeta-Tabelle

Die Rollen, die ein Benutzer hat, sind in der usermeta-Datenbanktabelle unter der jeweiligen Benutzer-ID als serialisiertes Array gespeichert.

Zusätzliche Berechtigungen können theoretisch ebenfalls Teil dieses Arrays sein, unabhängig von Rollen.

array(
	'editor' => true,
);

Richtlinien für Plugin-Entwickler

  1. Immer auf Berechtigungen prüfen, sich dabei nicht mit Rollen beschäftigen.
  2. Niemals Berechtigungen in der Datenbank anpassen, außer man führt eine komplett neue Rolle ein.
  3. Eigene Berechtigungen für den eigenen Code verwenden, anstatt sich auf bestehende Core-Berechtigungen zu verlassen.

Auf Berechtigungen prüfen

current_user_can( string $capability )

oder

user_can( int|WP_User $user, string $capability )

Arten von Berechtigungen

  • Primitive Berechtigungen
  • Meta-Berechtigungen

Primitive Berechtigungen

  • allgemeine Berechtigungen
  • gewährt entweder durch eine Rolle aus der Datenbank oder durch den user_has_cap-Filter
current_user_can( 'edit_posts' )
current_user_can( 'activate_plugins' )

Meta-Berechtigungen

  • Berechtigungen, die sich auf eine spezifische Sache beziehen (eine ID eines Posts, ein String-Bezeichner eines Plugins, ...)
  • werden mithilfe des map_meta_cap-Filters in ein oder mehrere primitive Berechtigungen gemappt/aufgelöst
current_user_can( 'edit_post', $post_id )
current_user_can( 'activate_plugin', $plugin_basename )

Spezielle Berechtigungen

Die folgenden Berechtigungen werden gesondert behandelt:

  • exist: Jeder verfügt über diese Berechtigung, sogar ausgeloggte Benutzer.
  • do_not_allow: Niemand verfügt über diese Berechtigung, nicht einmal Netzwerk-Administratoren.

Namenskonventionen

{action}_{items} für primitive Berechtigungen

{action}_{item} für Meta-Berechtigungen

Intern: Flow der Überprüfung einer Berechtigung

  1. Prüfen auf die Berechtigung
    current_user_can()
  2. Prüfen, ob es eine Meta-Berechtigung ist und diese in ihre erforderlichen primitiven Berechtigungen auflösen
    map_meta_cap()apply_filters( 'map_meta_cap' )
  3. Logik ausführen, um möglicherweise die primitiven Berechtigungen des Benutzers aus der Datenbank dynamisch zu verändern/erweitern
    apply_filters( 'user_has_cap' )
  4. Wenn die primitiven Berechtigungen des Benutzers alle erforderlichen primitiven Berechtigungen umfassen, ist die Überprüfung erfolgreich.

Verwaltung von Berechtigungen in Plugins

Unser kleines Plugin: "Capability Tutorials"

github.com/felixarntz/capability-tutorials

  • fügt einen Tutorial-Post Type hinzu
  • fügt einen Einstellungs-Bildschirm mit einigen Optionen zur Anpassung des Verhaltens des Post Types ein

Auf Berechtigungen prüfen

current_user_can( string $capability )

developer.wordpress.org/reference/functions/current_user_can/

function ct_add_settings_page() {
	$hook_suffix = add_submenu_page(
		'edit.php?post_type=' . CT_POST_TYPE_SINGULAR,
		__( 'Tutorial Settings', 'capability-tutorials' ),
		__( 'Settings', 'capability-tutorials' ),
		'manage_ct_options',
		'ct_settings_page',
		'ct_render_settings_page'
	);

	add_action( "load-{$hook_suffix}", 'ct_initialize_settings_page' );
}
add_action( 'admin_menu', 'ct_add_settings_page' );

add_menu_page() und add_submenu_page() sind Beispiele, wo eine Berechtigung angegeben werden muss. WordPress verwendet intern current_user_can() für die entsprechende Überprüfung.

Ohne Berechtigungs-Überprüfungen

function ct_initialize_settings_page() {
	add_settings_field( 'rewrite_slug', __( 'Rewrite Slug', 'capability-tutorials' ), 'ct_render_settings_page_rewrite_slug_field', CT_OPTION_GROUP, 'default', array(
		'label_for' => 'ct-rewrite-slug',
	) );
	add_settings_field( 'supports', __( 'Supported Features', 'capability-tutorials' ), 'ct_render_settings_page_supports_field', CT_OPTION_GROUP, 'default' );
	add_settings_field( 'has_archive', __( 'Archive', 'capability-tutorials' ), 'ct_render_settings_page_has_archive_field', CT_OPTION_GROUP, 'default' );
}

Mit Berechtigungs-Überprüfungen

function ct_initialize_settings_page() {
	if ( current_user_can( 'manage_ct_option', 'ct_rewrite_slug' ) ) {
		add_settings_field( 'rewrite_slug', __( 'Rewrite Slug', 'capability-tutorials' ), 'ct_render_settings_page_rewrite_slug_field', CT_OPTION_GROUP, 'default', array(
			'label_for' => 'ct-rewrite-slug',
		) );
	}
	if ( current_user_can( 'manage_ct_option', 'ct_supports' ) ) {
		add_settings_field( 'supports', __( 'Supported Features', 'capability-tutorials' ), 'ct_render_settings_page_supports_field', CT_OPTION_GROUP, 'default' );
	}
	if ( current_user_can( 'manage_ct_option', 'ct_has_archive' ) ) {
		add_settings_field( 'has_archive', __( 'Archive', 'capability-tutorials' ), 'ct_render_settings_page_has_archive_field', CT_OPTION_GROUP, 'default' );
	}
}
WTF?

→ Die Berechtigungen müssen noch passend gewährt werden!

Primitive Berechtigungen gewähren

apply_filters( 'user_has_cap', array $allcaps, array $caps, array $args, WP_User $user )

developer.wordpress.org/reference/hooks/user_has_cap/

function ct_maybe_grant_capabilities( $allcaps ) {
	// Allow managing plugin options depending on the user having the 'manage_options' capability.
	if ( isset( $allcaps['manage_options'] ) ) {
		$allcaps['manage_ct_options'] = $allcaps['manage_options'];
	}

	return $allcaps;
}
add_filter( 'user_has_cap', 'ct_maybe_grant_capabilities' );
WTF?

→ Die Meta-Berechtigungen müssen in primitive Berechtigungen aufgelöst werden!

Meta-Berechtigungen auflösen

apply_filters( 'map_meta_cap', array $caps, string $cap, int $user_id, array $args )

developer.wordpress.org/reference/hooks/map_meta_cap/

function ct_map_meta_capabilities( $caps, $cap, $user_id, $args ) {
	switch ( $cap ) {
		// Maps the meta capability for a single option to the primitive capability for options.
		case 'manage_ct_option':
			$caps = array( 'manage_ct_options' );
			break;
	}

	return $caps;
}
add_filter( 'map_meta_cap', 'ct_map_meta_capabilities', 10, 4 );

Und was soll das alles jetzt?

Während das Standardverhalten sich durch die granulare Nutzung von Berechtigungen nicht verändert, erlaubt dies anderen Entwicklern folgende projektspezifische Verbesserungen:

  • Security
  • Usability
  • Customizability

Beispiel 1

Anstatt den user_has_cap-Filter zur Gewährung der Berechtigungen zu verwenden, könnte eine eigene "Tutorial Manager"-Rolle eingeführt werden und diese dann die Berechtigungen allen Benutzern mit dieser Rolle gewähren.

function mysetup_add_tutorial_manager_role() {
	add_role( 'tutorial_manager', __( 'Tutorial Manager', 'my-setup' ), array(
		'edit_ct_tutorials'             => true,
		'edit_others_ct_tutorials'      => true,
		'publish_ct_tutorials'          => true,
		'read_private_ct_tutorials'     => true,
		'create_ct_tutorials'           => true,
		'edit_private_ct_tutorials'     => true,
		'edit_published_ct_tutorials'   => true,
		'delete_ct_tutorials'           => true,
		'delete_others_ct_tutorials'    => true,
		'delete_private_ct_tutorials'   => true,
		'delete_published_ct_tutorials' => true,
		'read_ct_tutorials'             => true,
		'manage_ct_options'             => true,
	) );
}
register_activation_hook( __FILE__, 'mysetup_add_tutorial_manager_role' );

remove_filter( 'user_has_cap', 'ct_maybe_grant_capabilities' );

Beispiel 2

In einer Multisite könnte man beispielsweise wollen, dass die Rewrite Slug-Option nur vom Netzwerk-Administrator (oder jemand anderem, der über die manage_network_options-Berechtigung verfügt) verändert werden kann.

function mysetup_tweak_rewrite_slug_capability( $caps, $cap, $user_id, $args ) {
	if ( 'manage_ct_option' === $cap ) {
		$caps = array( 'manage_ct_options' );
		if ( ! empty( $args ) && 'ct_rewrite_slug' === $args[0] && is_multisite() ) {
			$caps[] = 'manage_network_options';
		}
	}

	return $caps;
}
add_filter( 'map_meta_cap', 'mysetup_tweak_rewrite_slug_capability', 11, 4 );

Unterstützung von Multisite

Der Netzwerk-Administrator (manchmal auch "Super Admin" genannt) scheint erstmal eine zusätzliche Rolle zu sein. Eigentlich ist es jedoch nur eine einfache Flag, die einem Benutzer erlaubt, auf alle Berechtigungen außer do_not_allow zugreifen zu können.

Nichtsdestotrotz sollte immer auf Berechtigungen geprüft werden, mithilfe eigener multisite-spezifischer Berechtigungen. Niemals is_super_admin() verwenden!

Primitive Multisite-Berechtigungen im Core

  • manage_sites
  • create_sites
  • delete_sites
  • manage_network
  • manage_network_themes
  • manage_network_plugins
  • manage_network_users
  • manage_network_options
  • upgrade_network

Unterschiede zwischen Multisite und Single-Site

Oftmals möchte man in Multisite eine Berechtigung ausschließlich Netzwerk-Administratoren gewähren, während ansonsten auch reguläre Administratoren darauf Zugriff haben sollten.

Nicht so toll

function map_install_plugin_cap( $caps, $cap, $user_id, $args ) {
	if ( 'install_plugin' === $cap ) {
		$caps = array( 'install_plugins' );
		if ( is_multisite() && ! is_super_admin( $user_id ) ) {
			$caps[] = 'do_not_allow';
		}
	}
	return $caps;
}

Deutlich besser

function map_install_plugin_cap( $caps, $cap, $user_id, $args ) {
	if ( 'install_plugin' === $cap ) {
		$caps = array( 'install_plugins' );
		if ( is_multisite() ) {
			$caps[] = 'manage_network_plugins';
		}
	}
	return $caps;
}

Vielen Dank!

Felix Arntz

Plugin Developer / Core Committer / Freelancer

Interessiert an noch mehr?