diff --git a/src/models/groups.js b/src/models/groups.js
new file mode 100644
index 0000000000000000000000000000000000000000..b33be04aa5cc589d28ad8f2df5da8dd1575a3d21
--- /dev/null
+++ b/src/models/groups.js
@@ -0,0 +1,97 @@
+import { apiUrl } from './config';
+import { getToken, getUserId } from './auth';
+import { error } from './log';
+
+const m = require('mithril');
+
+let querySaved = '';
+
+export function getList() {
+  if (this.groups === undefined) {
+    return [];
+  }
+  return this.groups;
+}
+
+export function getMemberships() {
+  if (this.memberships === undefined) {
+    return [];
+  }
+  return this.memberships;
+}
+
+export function enroll(groupId) {
+  return m.request({
+    method: 'POST',
+    url: `${apiUrl}/groupmemberships`,
+    headers: getToken() ? {
+      Authorization: `Token ${getToken()}`,
+    } : {},
+    data: { group: groupId, user: getUserId() },
+  }).then((result) => {
+    const membership = result;
+    const group = this.groups.find(item => item._id === membership.group);
+    if (group === undefined) {
+      membership.group = membership.group;
+    } else {
+      membership.group = group;
+    }
+    this.memberships.push(membership);
+  }).catch((e) => {
+    error(e.message);
+  });
+}
+
+export function withdraw(groupMembershipId, etag) {
+  return m.request({
+    method: 'DELETE',
+    url: `${apiUrl}/groupmemberships/${groupMembershipId}`,
+    headers: getToken() ? {
+      Authorization: `Token ${getToken()}`,
+      'If-Match': etag,
+    } : { 'If-Match': etag },
+  }).then(() => {
+    this.memberships = this.memberships.filter(item => item._id !== groupMembershipId);
+  }).catch((e) => {
+    error(e.message);
+  });
+}
+
+export function load(query = {}) {
+  const queryEncoded = m.buildQueryString({ where: JSON.stringify(query) });
+  querySaved = query;
+
+  return m.request({
+    method: 'GET',
+    url: `${apiUrl}/groups?${queryEncoded}`,
+    headers: getToken() ? {
+      Authorization: `Token ${getToken()}`,
+    } : {},
+  }).then((result) => {
+    this.groups = result._items;
+  }).catch((e) => {
+    error(e.message);
+  });
+}
+
+export function loadMemberships() {
+  const queryEncoded = m.buildQueryString({
+    where: JSON.stringify({ user: getUserId() }),
+    embedded: JSON.stringify({ group: 1 }),
+  });
+  return m.request({
+    method: 'GET',
+    url: `${apiUrl}/groupmemberships?${queryEncoded}`,
+    headers: getToken() ? {
+      Authorization: `Token ${getToken()}`,
+    } : {},
+  }).then((result) => {
+    this.memberships = result._items;
+  }).catch((e) => {
+    error(e.message);
+  });
+}
+
+export function reload() {
+  return load(querySaved);
+}
diff --git a/src/views/profile.js b/src/views/profile.js
index e43e85b82dfe4c2efe494d46f7b74f69b5a40b76..6a3866c8ab70c8c8af1c56bb35ed22ab9b1e4999 100644
--- a/src/views/profile.js
+++ b/src/views/profile.js
@@ -1,5 +1,6 @@
 import { isLoggedIn } from '../models/auth';
 import * as user from '../models/user';
+import * as groups from '../models/groups';
 import { log } from '../models/log';
 
 const m = require('mithril');
@@ -94,6 +95,33 @@ class announceSubscriptionForm {
   }
 }
 
+// shows group memberships and allows to withdraw and enroll for selected groups.
+class groupMemberships {
+  static oninit() {
+    groups.load({ allow_self_enrollment: true });
+    groups.loadMemberships();
+  }
+
+  static view() {
+    return m('div', [
+      m('div', groups.getMemberships().map(membership => m('div', [
+        m('span', membership.group.name),
+        membership.expiry === undefined ? undefined : m('span', `(expires on ${membership.expiry})`),
+        m('button', { onclick: () => groups.withdraw(membership._id, membership._etag) }, 'withdraw'),
+      ]))),
+      m('div', groups.getList().map((group) => {
+        if (groups.getMemberships().some(element => element.group._id === group._id)) {
+          return undefined;
+        }
+        return m('div', [
+          m('span', group.name),
+          m('button', { onclick: () => groups.enroll(group._id) }, 'enroll'),
+        ]);
+      })),
+    ]);
+  }
+}
+
 // shows all submodules defined above
 export default class profile {
   static oninit() {
@@ -108,6 +136,7 @@ export default class profile {
       m(showUserInfo),
       m(rfidForm),
       m(announceSubscriptionForm),
+      m(groupMemberships),
     ]);
   }
 }