lib: Add new WpPermissionManager API

This allows setting object specific permissions on any client easily.
This commit is contained in:
Julian Bouzas 2026-01-30 13:44:50 -05:00
parent 1cfeab9a86
commit b262bbbd25
5 changed files with 783 additions and 0 deletions

View file

@ -22,6 +22,7 @@ wp_lib_sources = files(
'object.c',
'object-interest.c',
'object-manager.c',
'permission-manager.c',
'plugin.c',
'port.c',
'proc-utils.c',
@ -69,6 +70,7 @@ wp_lib_headers = files(
'object.h',
'object-interest.h',
'object-manager.h',
'permission-manager.h',
'plugin.h',
'port.h',
'proc-utils.h',

670
lib/wp/permission-manager.c Normal file
View file

@ -0,0 +1,670 @@
/* WirePlumber
*
* Copyright © 2026 Collabora Ltd.
* @author Julian Bouzas <julian.bouzas@collabora.com>
*
* SPDX-License-Identifier: MIT
*/
#include <pipewire/permission.h>
#include <pipewire/pipewire.h>
#include "private/permission-manager.h"
#include "permission-manager.h"
#include "proxy-interfaces.h"
#include "object-manager.h"
#include "json-utils.h"
#include "error.h"
#include "core.h"
#include "log.h"
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-permission-manager")
/*! \defgroup wppermissionmanager WpPermissionManager */
/*!
* \struct WpPermissionManager
*
* The WpPermissionManager class is in charge of updating automatically
* permissions on interested objects every time they are added or removed for
* a particular client.
*
* WpPermissionManager API.
*/
typedef struct _PermissionMatch PermissionMatch;
struct _PermissionMatch
{
guint32 id;
guint32 permissions;
GClosure *closure;
WpObjectInterest *interest;
WpSpaJson *rules;
};
static guint
get_next_id ()
{
static guint32 next_id = 0;
g_atomic_int_inc (&next_id);
return next_id;
}
static PermissionMatch *
permission_match_new (guint32 perms, GClosure *closure,
WpObjectInterest * interest, WpSpaJson * rules)
{
PermissionMatch *match = g_new0 (PermissionMatch, 1);
match->id = get_next_id ();
match->permissions = perms;
match->closure = closure ? g_closure_ref (closure) : NULL;
match->interest = interest ? wp_object_interest_ref (interest) : NULL;
match->rules = rules ? wp_spa_json_ref (rules) : NULL;
return match;
}
static void
permission_interest_free (PermissionMatch *self)
{
g_clear_pointer (&self->closure, g_closure_unref);
g_clear_pointer (&self->interest, wp_object_interest_unref);
g_clear_pointer (&self->rules, wp_spa_json_unref);
g_free (self);
}
struct _WpPermissionManager
{
WpObject parent;
guint32 default_perms;
GPtrArray *clients;
GHashTable *matches;
WpObjectManager *om;
};
G_DEFINE_TYPE (WpPermissionManager, wp_permission_manager, WP_TYPE_OBJECT)
static void
wp_permission_manager_init (WpPermissionManager * self)
{
/* Init default permissions to all */
self->default_perms = PW_PERM_R | PW_PERM_W | PW_PERM_X;
/* Init permission interests table */
self->matches = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL,
(GDestroyNotify)permission_interest_free);
/* Init clients list */
self->clients = g_ptr_array_new_with_free_func (
(GDestroyNotify) g_object_unref);
}
enum {
STEP_LOAD = WP_TRANSITION_STEP_CUSTOM_START,
};
static WpObjectFeatures
wp_permission_manager_get_supported_features (WpObject * self)
{
return WP_PERMISSION_MANAGER_LOADED;
}
static guint
wp_permission_manager_activate_get_next_step (WpObject * self,
WpFeatureActivationTransition * transition, guint step,
WpObjectFeatures missing)
{
g_return_val_if_fail (missing == WP_PERMISSION_MANAGER_LOADED,
WP_TRANSITION_STEP_ERROR);
return STEP_LOAD;
}
static guint32
invoke_permissions_closure (WpPermissionManager *self, WpClient *client,
WpGlobalProxy *object, GClosure *closure)
{
GValue args[3] = { G_VALUE_INIT, G_VALUE_INIT, G_VALUE_INIT };
GValue ret = G_VALUE_INIT;
guint32 perms;
g_value_init (&args[0], WP_TYPE_PERMISSION_MANAGER);
g_value_set_object (&args[0], self);
g_value_init (&args[1], WP_TYPE_CLIENT);
g_value_set_object (&args[1], client);
g_value_init (&args[2], WP_TYPE_GLOBAL_PROXY);
g_value_set_object (&args[2], object);
g_value_init (&ret, G_TYPE_UINT);
g_closure_invoke (closure, &ret, 3, args, NULL);
perms = g_value_get_uint (&ret);
g_value_unset (&args[0]);
g_value_unset (&args[1]);
g_value_unset (&args[2]);
g_value_unset (&ret);
return perms;
}
typedef struct _MatchRulesCallbackData MatchRulesCallbackData;
struct _MatchRulesCallbackData {
gboolean matched;
guint32 perms;
};
static gboolean
match_rules_cb (gpointer data, const gchar * action, WpSpaJson * value,
GError ** e)
{
MatchRulesCallbackData *cb_data = (MatchRulesCallbackData *)data;
g_autofree gchar *perms_str = NULL;
guint32 perms = 0;
if (!g_str_equal (action, "set-permissions")) {
if (e)
g_set_error (e, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
"Action name '%s' is not valid", action);
return FALSE;
}
if (!wp_spa_json_is_string (value)) {
if (e)
g_set_error (e, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
"Action '%s' must be a string", action);
return FALSE;
}
/* Parse permissions */
perms_str = wp_spa_json_parse_string (value);
if (g_strcmp0 (perms_str, "all") == 0) {
perms = PW_PERM_ALL;
} else {
for (guint i = 0; i < strlen (perms_str); i++) {
switch (perms_str[i]) {
case 'r': perms |= PW_PERM_R; break;
case 'w': perms |= PW_PERM_W; break;
case 'x': perms |= PW_PERM_X; break;
case 'm': perms |= PW_PERM_M; break;
case 'l': perms |= PW_PERM_L; break;
case '-': break;
default: {
if (e)
g_set_error (e, WP_DOMAIN_LIBRARY,
WP_LIBRARY_ERROR_INVALID_ARGUMENT,
"Permissions '%s' are not valid", perms_str);
return FALSE;
}
}
}
}
if (cb_data) {
cb_data->matched = TRUE;
cb_data->perms |= perms;
}
return TRUE;
}
static gboolean
get_rules_matched_object_permissions (WpPermissionManager *self,
WpSpaJson *rules, WpGlobalProxy *object, guint32 *perms)
{
g_autoptr (GError) e = NULL;
g_autoptr (WpProperties) gp_props = NULL;
g_autoptr (WpProperties) po_props = NULL;
MatchRulesCallbackData data = { FALSE, 0 };
/* Check global proxy properties */
gp_props = wp_global_proxy_get_global_properties (object);
if (gp_props && !wp_json_utils_match_rules (rules, gp_props, match_rules_cb,
&data, &e))
goto error;
/* Also check pipewire object properties if it is a pipewire object */
if (WP_IS_PIPEWIRE_OBJECT (object)) {
po_props = wp_pipewire_object_get_properties (WP_PIPEWIRE_OBJECT (object));
if (po_props && !wp_json_utils_match_rules (rules, po_props, match_rules_cb,
&data, &e))
goto error;
}
/* Set permissions if there was a match */
if (data.matched && perms)
*perms = data.perms;
return data.matched;
error:
wp_warning_object (self, "Malformed JSON match rules: %s", e->message);
return FALSE;
}
static gboolean
get_matched_object_permissions (WpPermissionManager *self, PermissionMatch *m,
WpClient *client, WpGlobalProxy *object, guint32 *perms)
{
/* Check interest */
if (m->interest && wp_object_interest_matches (m->interest, object)) {
if (!perms)
return TRUE;
*perms = m->closure ? invoke_permissions_closure (self, client, object,
m->closure) : m->permissions;
return TRUE;
}
/* Check rules */
if (m->rules)
return get_rules_matched_object_permissions (self, m->rules, object, perms);
return FALSE;
}
static GArray *
build_permissions_array (WpPermissionManager *self, WpClient *client)
{
g_autoptr (WpIterator) it = NULL;
g_auto (GValue) value = G_VALUE_INIT;
struct pw_permission def_perm = { PW_ID_ANY, self->default_perms };
GArray *arr = g_array_new (FALSE, FALSE, sizeof (struct pw_permission));
/* Add default permissions */
g_array_append_val (arr, def_perm);
/* Add object specific permissions in the array */
it = wp_object_manager_new_iterator (self->om);
for (; wp_iterator_next (it, &value); g_value_unset (&value)) {
WpGlobalProxy *object = g_value_get_object (&value);
GHashTableIter iter;
PermissionMatch *match = NULL;
g_hash_table_iter_init (&iter, self->matches);
while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&match)) {
guint32 perms = PW_PERM_INVALID;
if (get_matched_object_permissions (self, match, client, object, &perms)
&& perms != PW_PERM_INVALID) {
struct pw_permission obj_perm = { 0, };
obj_perm.id = wp_proxy_get_bound_id (WP_PROXY (object));
obj_perm.permissions = perms;
g_array_append_val (arr, obj_perm);;
}
}
}
/* Merge permissions with same object ID */
for (guint i = 0; i < arr->len; i++) {
for (guint j = i + 1; j < arr->len; ) {
struct pw_permission *a = &g_array_index (arr, struct pw_permission, i);
struct pw_permission *b = &g_array_index (arr, struct pw_permission, j);
if (a->id == b->id) {
a->permissions |= b->permissions;
g_array_remove_index (arr, j);
} else {
j++;
}
}
}
return arr;
}
static void
update_client_permissions (WpPermissionManager *self, WpClient *client)
{
guint32 bound_id = 0;
g_autoptr (GArray) perms = NULL;
/* Dont do anything if the permission manager is not activated */
if (!(wp_object_get_active_features (WP_OBJECT (self)) &
WP_PERMISSION_MANAGER_LOADED))
return;
/* Make sure the client proxy is still valid */
if (!wp_proxy_get_pw_proxy (WP_PROXY (client)))
return;
bound_id = wp_proxy_get_bound_id (WP_PROXY (client));
perms = build_permissions_array (self, client);
wp_info_object (self,
"Updating permissions on client %u: any=%c%c%c%c%c len=%u",
bound_id,
!!(self->default_perms & PW_PERM_R) ? 'r' : '-',
!!(self->default_perms & PW_PERM_W) ? 'w' : '-',
!!(self->default_perms & PW_PERM_X) ? 'x' : '-',
!!(self->default_perms & PW_PERM_M) ? 'm' : '-',
!!(self->default_perms & PW_PERM_L) ? 'l' : '-',
perms->len);
wp_client_update_permissions_array (client, perms->len,
(const struct pw_permission *) perms->data);
}
static gboolean
has_object_match (WpPermissionManager *self, WpGlobalProxy *object)
{
GHashTableIter iter;
PermissionMatch *m = NULL;
g_hash_table_iter_init (&iter, self->matches);
while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&m)) {
if (m->interest && wp_object_interest_matches (m->interest, object))
return TRUE;
if (m->rules && get_rules_matched_object_permissions (self, m->rules,
object, NULL))
return TRUE;
}
return FALSE;
}
static void
update_permissions (WpPermissionManager *self)
{
for (guint i = 0; i < self->clients->len; i++) {
WpClient *client = g_ptr_array_index (self->clients, i);
update_client_permissions (self, client);
}
}
static void
on_object_added_or_removed (WpObjectManager *om, WpGlobalProxy *object,
gpointer d)
{
WpPermissionManager * self = WP_PERMISSION_MANAGER (d);
if (has_object_match (self, object))
update_permissions (self);
}
static void
on_object_manager_installed (WpObjectManager *om, gpointer d)
{
WpTransition * transition = WP_TRANSITION (d);
WpPermissionManager * self = wp_transition_get_source_object (transition);
wp_object_update_features (WP_OBJECT (self), WP_PERMISSION_MANAGER_LOADED, 0);
}
static void
wp_permission_manager_activate_execute_step (WpObject * object,
WpFeatureActivationTransition * transition, guint step,
WpObjectFeatures missing)
{
WpPermissionManager *self = WP_PERMISSION_MANAGER (object);
g_autoptr (WpCore) core = wp_object_get_core (object);
switch (step) {
case STEP_LOAD: {
/* Install object manager */
g_clear_object (&self->om);
self->om = wp_object_manager_new ();
wp_object_manager_add_interest (self->om, WP_TYPE_GLOBAL_PROXY, NULL);
wp_object_manager_request_object_features (self->om,
WP_TYPE_GLOBAL_PROXY, WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL);
g_signal_connect_object (self->om, "object-added",
G_CALLBACK (on_object_added_or_removed), self, 0);
g_signal_connect_object (self->om, "object-removed",
G_CALLBACK (on_object_added_or_removed), self, 0);
g_signal_connect_object (self->om, "installed",
G_CALLBACK (on_object_manager_installed), transition, 0);
wp_core_install_object_manager (core, self->om);
break;
}
case WP_TRANSITION_STEP_ERROR:
break;
default:
g_assert_not_reached ();
}
}
static void
wp_permission_manager_deactivate (WpObject * object, WpObjectFeatures features)
{
WpPermissionManager *self = WP_PERMISSION_MANAGER (object);
g_clear_object (&self->om);
wp_object_update_features (WP_OBJECT (self), 0, WP_OBJECT_FEATURES_ALL);
}
static void
wp_permission_manager_finalize (GObject * object)
{
WpPermissionManager *self = WP_PERMISSION_MANAGER (object);
g_clear_pointer (&self->clients, g_ptr_array_unref);
g_clear_pointer (&self->matches, g_hash_table_unref);
g_clear_object (&self->om);
G_OBJECT_CLASS (wp_permission_manager_parent_class)->finalize (object);
}
static void
wp_permission_manager_class_init (WpPermissionManagerClass * klass)
{
GObjectClass * object_class = (GObjectClass *) klass;
WpObjectClass *wpobject_class = (WpObjectClass *) klass;
object_class->finalize = wp_permission_manager_finalize;
wpobject_class->get_supported_features =
wp_permission_manager_get_supported_features;
wpobject_class->activate_get_next_step =
wp_permission_manager_activate_get_next_step;
wpobject_class->activate_execute_step =
wp_permission_manager_activate_execute_step;
wpobject_class->deactivate = wp_permission_manager_deactivate;
}
void
wp_permission_manager_add_client (WpPermissionManager *self, WpClient *client)
{
g_return_if_fail (WP_IS_PERMISSION_MANAGER (self));
g_ptr_array_add (self->clients, g_object_ref (client));
update_client_permissions (self, client);
}
void
wp_permission_manager_remove_client (WpPermissionManager *self,
WpClient *client)
{
g_return_if_fail (WP_IS_PERMISSION_MANAGER (self));
g_ptr_array_remove_fast (self->clients, client);
update_client_permissions (self, client);
}
/*!
* \brief Creates a new WpPermissionManager object
*
* \ingroup wppermissionmanager
* \param core the WpCore
* \returns (transfer full): a new WpPermissionManager object
*/
WpPermissionManager *
wp_permission_manager_new (WpCore * core)
{
g_return_val_if_fail (core, NULL);
return g_object_new (WP_TYPE_PERMISSION_MANAGER, "core", core, NULL);
}
/*!
* \brief Sets the default permissions that will be applied to all objects that
* don't match any interest
*
* \ingroup wppermissionmanager
* \param self the permission manager
* \param permissions the default permissions to apply
*/
void
wp_permission_manager_set_default_permissions (WpPermissionManager *self,
guint32 permissions)
{
g_return_if_fail (WP_IS_PERMISSION_MANAGER (self));
if (self->default_perms != permissions) {
self->default_perms = permissions;
update_permissions (self);
}
}
static guint32
wp_permission_manager_add_match (WpPermissionManager *self,
PermissionMatch *match)
{
guint id = match->id;
g_hash_table_insert (self->matches, GUINT_TO_POINTER (id), match);
update_permissions (self);
return id;
}
/*!
* \brief Adds an interest match to apply permissions with callback in matched
* objects.
*
* Interest consists of a GType that the object must be an ancestor of
* (g_type_is_a() must match) and optionally, a set of additional constraints
* on certain properties of the object. Refer to WpObjectInterest for more details.
*
* \ingroup wppermissionmanager
* \param self the permission manager
* \param callback (scope async): the permissions match callback
* \param user_data data to pass to \a callback
* \param interest (transfer full): the interest
* \returns the added match ID, or SPA_ID_INVALID if error
*/
guint32
wp_permission_manager_add_interest_match (WpPermissionManager *self,
WpPermissionMatchCallback callback, gpointer user_data,
WpObjectInterest * interest)
{
GClosure *closure = g_cclosure_new (G_CALLBACK (callback), user_data, NULL);
return wp_permission_manager_add_interest_match_closure (self, closure,
interest);
}
/*!
* \brief Adds an interest match to apply permissions with closure in matched
* objects.
*
* Interest consists of a GType that the object must be an ancestor of
* (g_type_is_a() must match) and optionally, a set of additional constraints
* on certain properties of the object. Refer to WpObjectInterest for more details.
*
* \ingroup wppermissionmanager
* \param self the permission manager
* \param closure (transfer full): the closure to apply permissions
* \param interest (transfer full): the interest
* \returns the added match ID, or SPA_ID_INVALID if error
*/
guint32
wp_permission_manager_add_interest_match_closure (WpPermissionManager *self,
GClosure *closure, WpObjectInterest * interest)
{
g_autoptr (WpObjectInterest) i = interest;
g_autoptr (GClosure) c = closure;
PermissionMatch *match;
g_return_val_if_fail (WP_IS_PERMISSION_MANAGER (self), SPA_ID_INVALID);
g_return_val_if_fail (closure, SPA_ID_INVALID);
g_return_val_if_fail (i, SPA_ID_INVALID);
if (G_CLOSURE_NEEDS_MARSHAL (closure))
g_closure_set_marshal (closure, g_cclosure_marshal_generic);
match = permission_match_new (PW_PERM_INVALID, c, i, NULL);
return wp_permission_manager_add_match (self, match);
}
/*!
* \brief Adds an interest match to apply same permissions in matched objects.
*
* Interest consists of a GType that the object must be an ancestor of
* (g_type_is_a() must match) and optionally, a set of additional constraints
* on certain properties of the object. Refer to WpObjectInterest for more details.
*
* \ingroup wppermissionmanager
* \param self the permission manager
* \param permissions the permissions to apply
* \param interest (transfer full): the interest
* \returns the added match ID, or SPA_ID_INVALID if error
*/
guint32
wp_permission_manager_add_interest_match_simple (WpPermissionManager *self,
guint32 permissions, WpObjectInterest * interest)
{
g_autoptr (WpObjectInterest) i = interest;
PermissionMatch *match;
g_return_val_if_fail (WP_IS_PERMISSION_MANAGER (self), SPA_ID_INVALID);
g_return_val_if_fail (i, SPA_ID_INVALID);
match = permission_match_new (permissions, NULL, i, NULL);
return wp_permission_manager_add_match (self, match);
}
/*!
* \brief Adds a rules match to apply permissions in matched objects.
*
* The rules must be defined in a JSON object using the same format as all
* the wireplumber/pipewire rules.
*
* \ingroup wppermissionmanager
* \param self the permission manager
* \param rules (transfer full): the JSON rules
* \returns the added match ID, or SPA_ID_INVALID if error
*/
guint32
wp_permission_manager_add_rules_match (WpPermissionManager *self,
WpSpaJson *rules)
{
g_autoptr (WpSpaJson) r = rules;
PermissionMatch *match;
g_return_val_if_fail (WP_IS_PERMISSION_MANAGER (self), SPA_ID_INVALID);
g_return_val_if_fail (r, SPA_ID_INVALID);
match = permission_match_new (PW_PERM_INVALID, NULL, NULL, rules);
return wp_permission_manager_add_match (self, match);
}
/*!
* \brief Removes the previously added match so that the associated permissions
* are not applied anymore.
*
* \ingroup wppermissionmanager
* \param self the permission manager
* \param match_id the match ID to remove
*/
void
wp_permission_manager_remove_match (WpPermissionManager *self, guint32 match_id)
{
g_return_if_fail (WP_IS_PERMISSION_MANAGER (self));
g_return_if_fail (match_id != SPA_ID_INVALID);
g_hash_table_remove (self->matches, GUINT_TO_POINTER (match_id));
update_permissions (self);
}
/*!
* \brief Updates permissions on all clients the permission manager has.
*
* The permission manager already updates permissions on all clients
* automatically when a new client or object is added, however, this might be
* needed if interests with closures or callbacks were added and something
* changed externally.
*
* \ingroup wppermissionmanager
* \param self the permission manager
*/
void
wp_permission_manager_update_permissions (WpPermissionManager *self)
{
g_return_if_fail (WP_IS_PERMISSION_MANAGER (self));
update_permissions (self);
}

View file

@ -0,0 +1,84 @@
/* WirePlumber
*
* Copyright © 2026 Collabora Ltd.
* @author Julian Bouzas <julian.bouzas@ollabora.com>
*
* SPDX-License-Identifier: MIT
*/
#ifndef __WIREPLUMBER_PERMISSION_MANAGER_H__
#define __WIREPLUMBER_PERMISSION_MANAGER_H__
#include "object-interest.h"
#include "global-proxy.h"
G_BEGIN_DECLS
/*!
* \brief Flags to be used as WpObjectFeatures for WpPermissionManager.
* \ingroup wppermissionmanager
*/
typedef enum { /*< flags >*/
/*! Loads the permission manager */
WP_PERMISSION_MANAGER_LOADED = (1 << 0),
} WpPermissionManagerFeatures;
/*!
* \brief The WpPermissionManager GType
* \ingroup wppermissionmanager
*/
#define WP_TYPE_PERMISSION_MANAGER (wp_permission_manager_get_type ())
WP_API
G_DECLARE_FINAL_TYPE (WpPermissionManager, wp_permission_manager, WP,
PERMISSION_MANAGER, WpObject)
typedef struct _WpClient WpClient;
/*!
* \brief callback to set permissions on the matched global object
*
* \ingroup wpsettings
* \param self the permission manager
* \param client the client that will have its permissions updated
* \param object the matched global
* \param user_data the passed data
*/
typedef guint32 (*WpPermissionMatchCallback) (WpPermissionManager *self,
WpClient *client, WpGlobalProxy *object, gpointer user_data);
WP_API
WpPermissionManager * wp_permission_manager_new (WpCore * core);
WP_API
void wp_permission_manager_set_default_permissions (
WpPermissionManager *self, guint32 permissions);
WP_API
guint32 wp_permission_manager_add_interest_match (WpPermissionManager *self,
WpPermissionMatchCallback callback, gpointer user_data,
WpObjectInterest * interest);
WP_API
guint32 wp_permission_manager_add_interest_match_closure (
WpPermissionManager *self, GClosure *closure, WpObjectInterest * interest);
WP_API
guint32 wp_permission_manager_add_interest_match_simple (
WpPermissionManager *self, guint32 permissions,
WpObjectInterest * interest);
WP_API
guint32 wp_permission_manager_add_rules_match (WpPermissionManager *self,
WpSpaJson *rules);
WP_API
void wp_permission_manager_remove_match (WpPermissionManager *self,
guint32 match_id);
WP_API
void wp_permission_manager_update_permissions (WpPermissionManager *self);
G_END_DECLS
#endif

View file

@ -0,0 +1,26 @@
/* WirePlumber
*
* Copyright © 2026 Collabora Ltd.
* @author Julian Bouzas <julian.bouzas@collabora.com>
*
* SPDX-License-Identifier: MIT
*/
#ifndef __WIREPLUMBER_PRIVATE_PERMISSION_MANAGER_H__
#define __WIREPLUMBER_PRIVATE_PERMISSION_MANAGER_H__
#include "client.h"
G_BEGIN_DECLS
typedef struct _WpPermissionManager WpPermissionManager;
void wp_permission_manager_add_client (WpPermissionManager *self,
WpClient *client);
void wp_permission_manager_remove_client (WpPermissionManager *self,
WpClient *client);
G_END_DECLS
#endif

View file

@ -47,6 +47,7 @@
#include "wpversion.h"
#include "factory.h"
#include "settings.h"
#include "permission-manager.h"
G_BEGIN_DECLS