summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFrederic Guillot <fred@kanboard.net>2015-12-06 14:48:59 -0500
committerFrederic Guillot <fred@kanboard.net>2015-12-06 14:48:59 -0500
commit70c65268fe017014b97dbc18e02588b8ed1d9a19 (patch)
treed843b97b1458e696f7e9e993295e5961cc06903d
parent6d4286ec664f1df8b0e97b759075afbf34d9620a (diff)
Update plugin doc
-rw-r--r--app/Auth/LdapAuth.php27
-rw-r--r--app/Auth/ReverseProxyAuth.php10
-rw-r--r--app/Core/Ldap/User.php24
-rw-r--r--app/Model/User.php2
-rw-r--r--app/Template/user/edit.php2
-rw-r--r--doc/groupes.markdown3
-rw-r--r--doc/index.markdown3
-rw-r--r--doc/ldap-authentication.markdown2
-rw-r--r--doc/ldap-group-sync.markdown2
-rw-r--r--doc/ldap-parameters.markdown2
-rw-r--r--doc/plugin-authentication-architecture.markdown99
-rw-r--r--doc/plugin-authentication.markdown39
-rw-r--r--doc/plugin-authorization-architecture.markdown39
-rw-r--r--doc/plugin-group-provider.markdown55
-rw-r--r--doc/plugin-ldap-client.markdown99
-rw-r--r--doc/plugin-registration.markdown23
-rw-r--r--doc/plugins.markdown8
17 files changed, 380 insertions, 59 deletions
diff --git a/app/Auth/LdapAuth.php b/app/Auth/LdapAuth.php
index eb66e54d..85234ed3 100644
--- a/app/Auth/LdapAuth.php
+++ b/app/Auth/LdapAuth.php
@@ -23,7 +23,7 @@ class LdapAuth extends Base implements PasswordAuthenticationProviderInterface
* @access private
* @var \Kanboard\User\LdapUserProvider
*/
- private $user = null;
+ private $userInfo = null;
/**
* Username
@@ -62,8 +62,8 @@ class LdapAuth extends Base implements PasswordAuthenticationProviderInterface
{
try {
- $ldap = LdapClient::connect($this->getLdapUsername(), $this->getLdapPassword());
- $user = LdapUser::getUser($ldap, $this->getLdapUserPattern());
+ $client = LdapClient::connect($this->getLdapUsername(), $this->getLdapPassword());
+ $user = LdapUser::getUser($client, $this->username);
if ($user === null) {
$this->logger->info('User not found in LDAP server');
@@ -74,8 +74,8 @@ class LdapAuth extends Base implements PasswordAuthenticationProviderInterface
throw new LogicException('Username not found in LDAP profile, check the parameter LDAP_USER_ATTRIBUTE_USERNAME');
}
- if ($ldap->authenticate($user->getDn(), $this->password)) {
- $this->user = $user;
+ if ($client->authenticate($user->getDn(), $this->password)) {
+ $this->userInfo = $user;
return true;
}
@@ -94,7 +94,7 @@ class LdapAuth extends Base implements PasswordAuthenticationProviderInterface
*/
public function getUser()
{
- return $this->user;
+ return $this->userInfo;
}
/**
@@ -120,21 +120,6 @@ class LdapAuth extends Base implements PasswordAuthenticationProviderInterface
}
/**
- * Get LDAP user pattern
- *
- * @access public
- * @return string
- */
- public function getLdapUserPattern()
- {
- if (! LDAP_USER_FILTER) {
- throw new LogicException('LDAP user filter empty, check the parameter LDAP_USER_FILTER');
- }
-
- return sprintf(LDAP_USER_FILTER, $this->username);
- }
-
- /**
* Get LDAP username (proxy auth)
*
* @access public
diff --git a/app/Auth/ReverseProxyAuth.php b/app/Auth/ReverseProxyAuth.php
index 06573edb..b9730c5c 100644
--- a/app/Auth/ReverseProxyAuth.php
+++ b/app/Auth/ReverseProxyAuth.php
@@ -8,7 +8,7 @@ use Kanboard\Core\Security\SessionCheckProviderInterface;
use Kanboard\User\ReverseProxyUserProvider;
/**
- * ReverseProxy Authentication Provider
+ * Reverse-Proxy Authentication Provider
*
* @package auth
* @author Frederic Guillot
@@ -18,10 +18,10 @@ class ReverseProxyAuth extends Base implements PreAuthenticationProviderInterfac
/**
* User properties
*
- * @access private
+ * @access protected
* @var \Kanboard\User\ReverseProxyUserProvider
*/
- private $user = null;
+ protected $userInfo = null;
/**
* Get authentication provider name
@@ -45,7 +45,7 @@ class ReverseProxyAuth extends Base implements PreAuthenticationProviderInterfac
$username = $this->request->getRemoteUser();
if (! empty($username)) {
- $this->user = new ReverseProxyUserProvider($username);
+ $this->userInfo = new ReverseProxyUserProvider($username);
return true;
}
@@ -71,6 +71,6 @@ class ReverseProxyAuth extends Base implements PreAuthenticationProviderInterfac
*/
public function getUser()
{
- return $this->user;
+ return $this->userInfo;
}
}
diff --git a/app/Core/Ldap/User.php b/app/Core/Ldap/User.php
index ab8d7296..0c9df63f 100644
--- a/app/Core/Ldap/User.php
+++ b/app/Core/Ldap/User.php
@@ -34,18 +34,18 @@ class User
}
/**
- * Get user profile (helper)
+ * Get user profile
*
* @static
* @access public
* @param Client $client
- * @param string $query
+ * @param string $username
* @return array
*/
- public static function getUser(Client $client, $query)
+ public static function getUser(Client $client, $username)
{
$self = new self(new Query($client));
- return $self->find($query);
+ return $self->find($self->getLdapUserPattern($username));
}
/**
@@ -204,4 +204,20 @@ class User
return LDAP_USER_BASE_DN;
}
+
+ /**
+ * Get LDAP user pattern
+ *
+ * @access public
+ * @param string $username
+ * @return string
+ */
+ public function getLdapUserPattern($username)
+ {
+ if (! LDAP_USER_FILTER) {
+ throw new LogicException('LDAP user filter empty, check the parameter LDAP_USER_FILTER');
+ }
+
+ return sprintf(LDAP_USER_FILTER, $username);
+ }
}
diff --git a/app/Model/User.php b/app/Model/User.php
index 7142c258..50e9b310 100644
--- a/app/Model/User.php
+++ b/app/Model/User.php
@@ -289,7 +289,7 @@ class User extends Base
$result = $this->db->table(self::TABLE)->eq('id', $values['id'])->update($values);
// If the user is connected refresh his session
- if (SessionManager::isOpen() && $this->userSession->getId() == $values['id']) {
+ if ($this->userSession->getId() == $values['id']) {
$this->userSession->initialize($this->getById($this->userSession->getId()));
}
diff --git a/app/Template/user/edit.php b/app/Template/user/edit.php
index 1a7fb430..f7f67fb7 100644
--- a/app/Template/user/edit.php
+++ b/app/Template/user/edit.php
@@ -8,7 +8,7 @@
<?= $this->form->hidden('id', $values) ?>
<?= $this->form->label(t('Username'), 'username') ?>
- <?= $this->form->text('username', $values, $errors, array('required', $values['is_ldap_user'] == 1 ? 'readonly' : '', 'maxlength="50"')) ?>
+ <?= $this->form->text('username', $values, $errors, array('required', isset($values['is_ldap_user']) && $values['is_ldap_user'] == 1 ? 'readonly' : '', 'maxlength="50"')) ?>
<?= $this->form->label(t('Name'), 'name') ?>
<?= $this->form->text('name', $values, $errors) ?>
diff --git a/doc/groupes.markdown b/doc/groupes.markdown
new file mode 100644
index 00000000..e479ef5a
--- /dev/null
+++ b/doc/groupes.markdown
@@ -0,0 +1,3 @@
+Groups Management
+=================
+
diff --git a/doc/index.markdown b/doc/index.markdown
index 3311156f..e4f83885 100644
--- a/doc/index.markdown
+++ b/doc/index.markdown
@@ -46,10 +46,11 @@ Using Kanboard
- [Subtasks](subtasks.markdown)
- [Analytics for tasks](analytics-tasks.markdown)
-### Working with users
+### Working with users and groups
- [Roles](roles.markdown)
- [User Types](user-types.markdown)
+- [Group management](groups.markdown)
- [User management](user-management.markdown)
- [Notifications](notifications.markdown)
- [Two factor authentication](2fa.markdown)
diff --git a/doc/ldap-authentication.markdown b/doc/ldap-authentication.markdown
index c932ec5a..05131887 100644
--- a/doc/ldap-authentication.markdown
+++ b/doc/ldap-authentication.markdown
@@ -81,7 +81,7 @@ Examples:
Other examples of [filters for Active Directory](http://social.technet.microsoft.com/wiki/contents/articles/5392.active-directory-ldap-syntax-filters.aspx)
-By example you can filter access to Kanboard from the user filter:
+Example to filter access to Kanboard:
`(&(objectClass=user)(sAMAccountName=%s)(memberOf=CN=Kanboard Users,CN=Users,DC=kanboard,DC=local))`
diff --git a/doc/ldap-group-sync.markdown b/doc/ldap-group-sync.markdown
index 29489229..a5ced8db 100644
--- a/doc/ldap-group-sync.markdown
+++ b/doc/ldap-group-sync.markdown
@@ -52,6 +52,6 @@ define('LDAP_GROUP_FILTER', '(&(objectClass=group)(sAMAccountName=%s*))');
With the filter given as example above, Kanboard will search for groups that match the query.
If the end-user type the text "My group" in the auto-complete box, Kanboard will return all groups that match the pattern: `(&(objectClass=group)(sAMAccountName=My group*))`.
-Note that the special characters ***** is import here, otherwise an exact match will be done.
+Note that the special characters `*` is important here, otherwise an exact match will be done.
[More examples of LDAP filters for Active Directory](http://social.technet.microsoft.com/wiki/contents/articles/5392.active-directory-ldap-syntax-filters.aspx)
diff --git a/doc/ldap-parameters.markdown b/doc/ldap-parameters.markdown
index 8dd0565b..772a08c3 100644
--- a/doc/ldap-parameters.markdown
+++ b/doc/ldap-parameters.markdown
@@ -28,4 +28,6 @@ Here are the list of available LDAP parameters:
| `LDAP_GROUP_FILTER` | Empty | LDAP group filter (Example: "(&(objectClass=group)(sAMAccountName=%s*))") |
| `LDAP_GROUP_ATTRIBUTE_NAME` | cn | LDAP attribute for the group name |
+Notes:
+
- LDAP attributes must be in lowercase
diff --git a/doc/plugin-authentication-architecture.markdown b/doc/plugin-authentication-architecture.markdown
new file mode 100644
index 00000000..6ef1fb88
--- /dev/null
+++ b/doc/plugin-authentication-architecture.markdown
@@ -0,0 +1,99 @@
+Authentication Architecture
+===========================
+
+Kanboard provides a flexible and pluggable authentication architecture.
+
+By default, user authentication can be done with multiple methods:
+
+- Username and password authentication (Local database and LDAP)
+- OAuth2 authentication
+- Reverse-Proxy authentication
+- Cookie based authentication (Remember Me)
+
+More over, after a successful authentication, a Two-Factor post authentication can be done.
+Kanboard supports natively the TOTP standard.
+
+Authentication Interfaces
+-------------------------
+
+To have a pluggable system, auhentication drivers must implement a set of interfaces:
+
+| Interface | Role |
+|------------------------------------------|------------------------------------------------------------------|
+| AuthenticationProviderInterface | Base interface for other authentication interfaces |
+| PreAuthenticationProviderInterface | The user is already authenticated when reaching the application, web servers usually define some environment variables |
+| PasswordAuthenticationProviderInterface | Authentication methods that uses the username and password provided in the login form |
+| OAuthAuthenticationProviderInterface | OAuth2 providers |
+| PostAuthenticationProviderInterface | Two-Factor auhentication drivers, ask for confirmation code |
+| SessionCheckProviderInterface | Providers that are able to check if the user session is valid |
+
+### Examples of authentication providers:
+
+- The default Database method implements `PasswordAuthenticationProviderInterface` and `SessionCheckProviderInterface`
+- The Reverse-Proxy method implements `PreAuthenticationProviderInterface` and `SessionCheckProviderInterface`
+- The Google method implements `OAuthAuthenticationProviderInterface`
+- The LDAP method implements `PasswordAuthenticationProviderInterface`
+- The RememberMe cookie method implements `PreAuthenticationProviderInterface`
+- The Two-Factor TOTP method implements `PostAuthenticationProviderInterface`
+
+Authentication Workflow
+-----------------------
+
+For each HTTP request:
+
+1. If the user session is already open, execute registered providers that implements `SessionCheckProviderInterface`
+2. Execute all providers that implements `PreAuthenticationProviderInterface`
+3. If the end-user submit the login form, providers that implements `PasswordAuthenticationProviderInterface` are executed
+4. If the end-user want to use OAuth2, the selected provider will be executed
+5. After a successful authentication, the last registered `PostAuthenticationProviderInterface` will be used
+6. Synchronize user information if necessary
+
+This workflow is managed by the class `Kanboard\Core\Security\AuthenticationManager`.
+
+Events triggered:
+
+- `AuthenticationManager::EVENT_SUCCESS`: Successful authentication
+- `AuthenticationManager::EVENT_FAILURE`: Failed authentication
+
+Each time a failure event occurs, the counter of failed logins is incremented.
+
+The user account can be locked down for the configured period of time and a captcha can be shown to avoid bruteforce attacks.
+
+User Provider Interface
+-----------------------
+
+When the authentication is successful, the `AuthenticationManager` will ask the user information to your driver by calling the method `getUser()`.
+This method must return an object that implements the interface `Kanboard\Core\User\UserProviderInterface`.
+
+This class abstract the information gathered from another system.
+
+Examples:
+
+- `DatabaseUserProvider` provides information for an internal user
+- `LdapUserProvider` for a LDAP user
+- `ReverseProxyUserProvider` for a Reverse-Proxy user
+- `GoogleUserProvider` represents a Google user
+
+Methods for User Provider Interface:
+
+- `isUserCreationAllowed()`: Return true to allow automatic user creation
+- `getExternalIdColumn()`: Get external id column name (google_id, github_id, gitlab_id...)
+- `getInternalId()`: Get internal database id
+- `getExternalId()`: Get external id (Unique id)
+- `getRole()`: Get user role
+- `getUsername()`: Get username
+- `getName()`: Get user full name
+- `getEmail()`: Get user email address
+- `getExternalGroupIds()`: Get external group ids, automatically sync group membership if present
+- `getExtraAttributes()`: Get extra attributes to set for the user during the local sync
+
+It's not mandatory to return a value for each method.
+
+User Local Synchronization
+--------------------------
+
+User information can be automatically synced with the local database.
+
+- If the method `getInternalId()` return a value no synchronization is performed
+- The methods `getExternalIdColumn()` and `getExternalId()` must returns a value to sync the user
+- Properties that returns an empty string won't be synced
diff --git a/doc/plugin-authentication.markdown b/doc/plugin-authentication.markdown
new file mode 100644
index 00000000..30ccda7c
--- /dev/null
+++ b/doc/plugin-authentication.markdown
@@ -0,0 +1,39 @@
+Authentication Plugin
+=====================
+
+New authentication backends can be written with very few lines of code.
+
+Provider Registration
+---------------------
+
+In the method `initialize()` of your plugin, call the method `register()` of the class `AuthenticationManager`:
+
+```php
+public function initialize()
+{
+ $this->authenticationManager->register(new ReverseProxyLdapAuth($this->container));
+}
+```
+
+The object provided to the method `register()` must implement one of the pre-defined authentication interfaces.
+
+Those interfaces are defined in the namepsace `Kanboard\Core\Security`:
+
+- `Kanboard\Core\Security\PreAuthenticationProviderInterface`
+- `Kanboard\Core\Security\PostAuthenticationProviderInterface`
+- `Kanboard\Core\Security\PasswordAuthenticationProviderInterface`
+- `Kanboard\Core\Security\OAuthAuthenticationProviderInterface`
+
+The only requirement is to implement the interfaces, you class can be written the way you want and located anywhere on disk.
+
+User Provider
+-------------
+
+When the authentication is successful, your driver must return an object that represent the user.
+This object must implements the interface `Kanboard\Core\User\UserProviderInterface`.
+
+Example of authentication plugins
+---------------------------------
+
+- [Authentication providers included in Kanboard](https://github.com/fguillot/kanboard/tree/master/app/Auth)
+- [Reverse-Proxy Authentication with LDAP support](https://github.com/kanboard/plugin-reverse-proxy-ldap)
diff --git a/doc/plugin-authorization-architecture.markdown b/doc/plugin-authorization-architecture.markdown
new file mode 100644
index 00000000..332313f0
--- /dev/null
+++ b/doc/plugin-authorization-architecture.markdown
@@ -0,0 +1,39 @@
+Authorization Architecture
+==========================
+
+Kanboard [supports multiple roles](roles.markdown) at the application level and at the project level.
+
+Authorization Workflow
+----------------------
+
+For each HTTP request:
+
+1. Authorize or not access to the resource based on the application access list
+2. If the resource is for a project (board, task...):
+ 1. Fetch user role for this project
+ 2. Grant/Denied access based on the project access map
+
+Extending access Map
+--------------------
+
+The Access List (ACL) is based on the controller class name and the method name.
+The list of access are handled by the class `Kanboard\Core\Security\AccessMap`.
+
+There are two access map: one for the application and another one for projects.
+
+- Application access map: `$this->applicationAccessMap`
+- Project access map: `$this->projectAccessMap`
+
+Example to define a new policy from your plugin:
+
+```php
+// All methods of the class MyController:
+$this->projectAccessMap->add('MyController', '*', Role::PROJECT_MANAGER);
+
+// All some methods:
+$this->projectAccessMap->add('MyOtherController', array('create', 'save'), Role::PROJECT_MEMBER);
+```
+
+Roles are defined in the class `Kanboard\Core\Security\Role`.
+
+The Authorization class (`Kanboard\Core\Security\Authorization`) will check the access for each page.
diff --git a/doc/plugin-group-provider.markdown b/doc/plugin-group-provider.markdown
new file mode 100644
index 00000000..b62ecaa6
--- /dev/null
+++ b/doc/plugin-group-provider.markdown
@@ -0,0 +1,55 @@
+Custom Group Providers
+======================
+
+Kanboard is able to load groups from an external system.
+This feature is mainly used for project permissions.
+
+Project managers can allow access to a project for a group.
+The end-user will use an auto-complete box and search for a group.
+
+Each time a group query is executed, all registered group providers are executed.
+
+Group Provider Workflow
+-----------------------
+
+1. The end-user start to type the group name in the auto-complete field
+2. The `GroupManager` class will execute the query across all registered group providers
+3. Results are merged an returned to the user interface
+4. After selecting a group, the information of the group are synced to the local database if necessary
+
+Group Provider Interface
+------------------------
+
+Interface to implement: `Kanboard\Core\Group\GroupProviderInterface`.
+
+Classes that implements this interface abstract the group information, there are only 3 methods:
+
+- `getInternalId()`: Get internal database id, return 0 otherwise
+- `getExternalId()`: Get external unique id
+- `getName()`: Get group name
+
+Kanboard will use the external id to sync with the local database.
+
+Group Backend Provider Interface
+--------------------------------
+
+Interface to implement: `Kanboard\Core\Group\GroupBackendProviderInterface`.
+
+This interface require only one method: `find($input)`.
+The argument `$input` is the text entered from the user interface.
+
+This method must return a list of `GroupProviderInterface`, this is the result of the search.
+
+Backend Registration from Plugins
+---------------------------------
+
+In the method `initialize()` of your plugin register your custom backend like that:
+
+```php
+$groupManager->register(new MyCustomLdapBackendGroupProvider($this->container));
+```
+
+Examples
+--------
+
+- [Group providers included in Kanboard (LDAP and Database)](https://github.com/fguillot/kanboard/tree/master/app/Group)
diff --git a/doc/plugin-ldap-client.markdown b/doc/plugin-ldap-client.markdown
new file mode 100644
index 00000000..c7fece8b
--- /dev/null
+++ b/doc/plugin-ldap-client.markdown
@@ -0,0 +1,99 @@
+LDAP Library
+============
+
+To facilitate LDAP integrations, Kanboard have its own LDAP library.
+This library can perform common operations.
+
+Client
+------
+
+Class: `Kanboard\Core\Ldap\Client`
+
+To connect to your LDAP server easily, use this method:
+
+```php
+use Kanboard\Core\Ldap\Client as LdapClient;
+use Kanboard\Core\Ldap\ClientException as LdapException;
+
+try {
+ $client = LdapClient::connect();
+
+ // Get native LDAP resource
+ $resource = $client->getConnection();
+
+ // ...
+
+} catch (LdapException $e) {
+ // ...
+}
+```
+
+LDAP Queries
+------------
+
+Classes:
+
+- `Kanboard\Core\Ldap\Query`
+- `Kanboard\Core\Ldap\Entries`
+- `Kanboard\Core\Ldap\Entry`
+
+Example to query the LDAP directory:
+
+```php
+
+$query = new Query($client)
+$query->execute('ou=People,dc=kanboard,dc=local', 'uid=my_user', array('cn', 'mail'));
+
+if ($query->hasResult()) {
+ $entries = $query->getEntries(); // Return an instance of Entries
+}
+```
+
+Read one entry:
+
+```php
+$firstEntry = $query->getEntries()->getFirstEntry();
+$email = $firstEntry->getFirstValue('mail');
+$name = $firstEntry->getFirstValue('cn', 'Default Name');
+```
+
+Read multiple entries:
+
+```php
+foreach ($query->getEntries()->getAll() as $entry) {
+ $emails = $entry->getAll('mail'); // Fetch all emails
+ $dn = $entry->getDn(); // Get LDAP DN of this user
+
+ // Check if a value is present for an attribute
+ if ($entry->hasValue('mail', 'user2@localhost')) {
+ // ...
+ }
+}
+```
+
+User Helper
+-----------
+
+Class: `Kanboard\Core\Ldap\User`
+
+Fetch a single user in one line:
+
+```php
+// Return an instance of LdapUserProvider
+$user = User::getUser($client, 'my_username');
+```
+
+Group Helper
+------------
+
+Class: `Kanboard\Core\Ldap\Group`
+
+Fetch groups in one line:
+
+```php
+// Define LDAP filter
+$filter = '(&(objectClass=group)(sAMAccountName=My group*))';
+
+// Return a list of LdapGroupProvider
+$groups = Group::getGroups($client, $filter);
+```
diff --git a/doc/plugin-registration.markdown b/doc/plugin-registration.markdown
index 312f61b9..519eade0 100644
--- a/doc/plugin-registration.markdown
+++ b/doc/plugin-registration.markdown
@@ -175,26 +175,3 @@ The automatic action class must inherits from the class `Kanboard\Action\Base` a
- `hasRequiredCondition(array $data)`
For more details you should take a look to existing automatic actions or this [plugin example](https://github.com/kanboard/plugin-example-automatic-action).
-
-Extend ACL
-----------
-
-Kanboard use an access list for privilege separations. Your extension can add new rules:
-
-```php
-$this->acl->extend('project_manager_acl', array('mycontroller' => '*'));
-```
-
-- The first argument is the ACL name
-- The second argument are the new rules
- + Syntax to include only some actions: `array('controller' => array('action1', 'action2'))`
- + Syntax to include all actions of a controller: `array('controller' => '*')`
- + Everything is lowercase
-
-List of ACL:
-
-- `public_acl`: Public access without authentication
-- `project_member_acl`: Project member access
-- `project_manager_acl`: Project manager access
-- `project_admin_acl`: Project Admins
-- `admin_acl`: Administrators
diff --git a/doc/plugins.markdown b/doc/plugins.markdown
index 0a08fa8e..65d6dd1a 100644
--- a/doc/plugins.markdown
+++ b/doc/plugins.markdown
@@ -5,7 +5,7 @@ Note: The plugin API is **considered alpha** at the moment.
Plugins are useful to extend the core functionalities of Kanboard, adding features, creating themes or changing the default behavior.
-Plugin creators should specify explicitly the compatible versions of Kanboard. Internal code of Kanboard may change over the time and your plugin must be tested with new versions.
+Plugin creators should specify explicitly the compatible versions of Kanboard. Internal code of Kanboard may change over the time and your plugin must be tested with new versions. Always check the [ChangeLog](https://github.com/fguillot/kanboard/blob/master/ChangeLog) for breaking changes.
- [Creating your plugin](plugin-registration.markdown)
- [Using plugin hooks](plugin-hooks.markdown)
@@ -14,10 +14,16 @@ Plugin creators should specify explicitly the compatible versions of Kanboard. I
- [Add mail transports](plugin-mail-transports.markdown)
- [Add notification types](plugin-notifications.markdown)
- [Attach metadata to users, tasks and projects](plugin-metadata.markdown)
+- [Authentication architecture](plugin-authentication-architecture.markdown)
+- [Authentication plugin registration](plugin-authentication.markdown)
+- [Authorization Architecture](plugin-authorization-architecture.markdown)
+- [Custom Group Providers](plugin-group-provider.markdown)
+- [LDAP client](plugin-ldap-client.markdown)
Examples of plugins
-------------------
+- [Reverse-Proxy Authentication with LDAP support](https://github.com/kanboard/plugin-reverse-proxy-ldap)
- [Slack](https://github.com/kanboard/plugin-slack)
- [Hipchat](https://github.com/kanboard/plugin-hipchat)
- [Jabber](https://github.com/kanboard/plugin-jabber)