Skip to content

Commit

Permalink
Allow Group/Organization mapping to trigger invitation
Browse files Browse the repository at this point in the history
  • Loading branch information
Timshel committed Sep 2, 2024
1 parent 53f00b8 commit 572c94c
Show file tree
Hide file tree
Showing 18 changed files with 459 additions and 115 deletions.
8 changes: 8 additions & 0 deletions .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,14 @@
# SSO_ROLES_DEFAULT_TO_USER=true
## Id token path to read roles
# SSO_ROLES_TOKEN_PATH=/resource_access/${SSO_CLIENT_ID}/roles
## Controls whether to add users to organization
# SSO_ORGANIZATIONS_INVITE=false
## Id token path to read groups
# SSO_ORGANIZATIONS_TOKEN_PATH=/groups
## Organization ID mapping
# SSO_ORGANIZATIONS_ID_MAPPING="ProviderId:VaultwardenId;"
## Grant access to all the organization collections
# SSO_ORGANIZATIONS_ALL_COLLECTIONS=true
## Client cache for discovery endpoint. Duration in seconds (0 to disable).
# SSO_CLIENT_CACHE_EXPIRATION=0
## Log all the tokens, `LOG_LEVEL=debug` or `LOG_LEVEL=info,vaultwarden::sso=debug` need to be set
Expand Down
32 changes: 30 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ See [changelog](CHANGELOG.md) for more details.

## Additionnal features

This branch now contain additionnal features not added to the SSO [PR](https://github.com/dani-garcia/vaultwarden/pull/3899) since it would slow even more it's review.
This branch now contain features not added to the SSO [PR](https://github.com/dani-garcia/vaultwarden/pull/3899) since it would slow even more it's review.

### Role mapping

Expand All @@ -26,6 +26,34 @@ This feature is controlled by the following conf:
- `SSO_ROLES_DEFAULT_TO_USER`: do not block login in case of missing or invalid roles, default is `true`.
- `SSO_ROLES_TOKEN_PATH=/resource_access/${SSO_CLIENT_ID}/roles`: path to read roles in the Access token

### Group/Organization invitation mapping

Allow to invite user to existing Oganization if they are listed in the Access token.
If activated it will check if the token contain a list of potential Orgnaization.
If an Oganization with a matching name (case sensitive) is found it will the start the invitation process for this user.
It will use the email associated with the Organization to send further notifications (admin side).

The flow look like this:

- Decode the JWT Access token and check if a list of organization is present (default path is `/groups`).
- Check if an Organization with a matching name exist and the user is not part of it (Use group name mapping if `SSO_ORGANIZATIONS_ID_MAPPING` is defined).
- if mail are activated invite the user to the Orgnization
- The user will need to click on the link in the mail he received
- A notification is sent tto he `email` associated with the Organization that a new user is ready to join
- An admin will have to validate the user to finalize the user joining the org.
- Otherwise just add the user to the Organization
- An admin will have to validate the user to confirm the user joining the org.

One of the bonus of invitation is that if an organization define a specific password policy then it will apply to new user when they set their new master password.
If a user is part of two organizations then it will order them using the role of the user (`Owner`, `Admin`, `User` or `Manager` for now manager is last :() and return the password policy of the first one.

This feature is controlled with the following conf:

- `SSO_SCOPES`: Optional scope override if additionnal scopes are needed, default is `"email profile"`
- `SSO_ORGANIZATIONS_INVITE`: control if the mapping is done, default is `false`
- `SSO_ORGANIZATIONS_TOKEN_PATH`: path to read groups/organization in the Access token, default is `/groups`
- `SSO_ORGANIZATIONS_ID_MAPPING`: Optional, allow to map provider group to a Vaultwarden organization `uuid` (default `""`, format: `"ProviderId:VaultwardenId;"`)

### Experimental Version

Made a version which additionnaly allow to run the server without storing the master password (it's still required just not sent to the server).
Expand All @@ -37,7 +65,7 @@ Change the docker files to package both front-end from [Timshel/oidc_web_builds]
\
By default it will use the release which only make the `sso` button visible.

If you want to use the version which additionally change the default redirection to `/sso` and fix organization invitation to persist.
If you want to use the version with the additional features mentionned, default redirection to `/sso` and fix organization invitation.
You need to pass an env variable: `-e SSO_FRONTEND='override'` (cf [start.sh](docker/start.sh)).

Docker images available at:
Expand Down
4 changes: 4 additions & 0 deletions SSO.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ The following configurations are available
- `SSO_ROLES_ENABLED`: control if the mapping is done, default is `false`
- `SSO_ROLES_DEFAULT_TO_USER`: do not block login in case of missing or invalid roles, default is `true`.
- `SSO_ROLES_TOKEN_PATH=/resource_access/${SSO_CLIENT_ID}/roles`: path to read roles in the Id token
- `SSO_ORGANIZATIONS_INVITE`: control if the mapping is done, default is `false`
- `SSO_ORGANIZATIONS_TOKEN_PATH`: path to read groups/organization in the Id token
- `SSO_ORGANIZATIONS_ID_MAPPING`: Optional, allow to map provider group to a Vaultwarden organization `uuid` (default `""`, format: `"ProviderId:VaultwardenId;"`)
- `SSO_ORGANIZATIONS_ALL_COLLECTIONS`: Grant access to all collections, default is `true`
- `SSO_CLIENT_CACHE_EXPIRATION`: Cache calls to the discovery endpoint, duration in seconds, `0` to disable (default `0`);
- `SSO_DEBUG_TOKENS`: Log all tokens for easier debugging (default `false`, `LOG_LEVEL=debug` or `LOG_LEVEL=info,vaultwarden::sso=debug` need to be set)

Expand Down
4 changes: 2 additions & 2 deletions playwright/compose/keycloak/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ ARG KEYCLOAK_VERSION
SHELL ["/bin/bash", "-o", "pipefail", "-c"]

RUN apt-get update \
&& apt-get install -y ca-certificates curl wget \
&& apt-get install -y ca-certificates curl wget jq \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /
Expand All @@ -21,7 +21,7 @@ ARG KEYCLOAK_VERSION
SHELL ["/bin/bash", "-o", "pipefail", "-c"]

RUN apt-get update \
&& apt-get install -y ca-certificates curl wget \
&& apt-get install -y ca-certificates curl wget jq \
&& rm -rf /var/lib/apt/lists/*

ARG JAVA_URL
Expand Down
22 changes: 21 additions & 1 deletion playwright/compose/keycloak/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,31 @@ kcadm.sh create -r "$TEST_REALM" "client-scopes/$TEST_CLIENT_ROLES_SCOPE_ID/prot
-s 'config."access.token.claim"=false' \
-s 'config."userinfo.token.claim"=true'

## Create group mapping client scope
TEST_GROUPS_CLIENT_SCOPE_ID=$(kcadm.sh create -r "$TEST_REALM" client-scopes -s name=groups -s protocol=openid-connect -i)
kcadm.sh create -r "$TEST_REALM" "client-scopes/$TEST_GROUPS_CLIENT_SCOPE_ID/protocol-mappers/models" \
-s name=Groups \
-s protocol=openid-connect \
-s protocolMapper=oidc-group-membership-mapper \
-s consentRequired=false \
-s 'config."claim.name"=groups' \
-s 'config."full.path"=false' \
-s 'config."id.token.claim"=true' \
-s 'config."access.token.claim"=true' \
-s 'config."userinfo.token.claim"=true'

TEST_GROUP_ID=$(kcadm.sh create -r "$TEST_REALM" groups -s name=Test -i)

TEST_CLIENT_ID=$(kcadm.sh create -r "$TEST_REALM" clients -s "name=VaultWarden" -s "clientId=$SSO_CLIENT_ID" -s "secret=$SSO_CLIENT_SECRET" -s "redirectUris=[\"$DOMAIN/*\"]" -i)

## ADD Role mapping scope
kcadm.sh update -r "$TEST_REALM" "clients/$TEST_CLIENT_ID" --body "{\"optionalClientScopes\": [\"$TEST_CLIENT_ROLES_SCOPE_ID\"]}"
kcadm.sh update -r "$TEST_REALM" "clients/$TEST_CLIENT_ID/optional-client-scopes/$TEST_CLIENT_ROLES_SCOPE_ID"

## ADD Group mapping scope
kcadm.sh update -r "$TEST_REALM" "clients/$TEST_CLIENT_ID" --body "{\"optionalClientScopes\": [\"$TEST_GROUPS_CLIENT_SCOPE_ID\"]}"
kcadm.sh update -r "$TEST_REALM" "clients/$TEST_CLIENT_ID/optional-client-scopes/$TEST_GROUPS_CLIENT_SCOPE_ID"

## CREATE TEST ROLES
kcadm.sh create -r "$TEST_REALM" "clients/$TEST_CLIENT_ID/roles" -s name=admin -s 'description=Admin role'
kcadm.sh create -r "$TEST_REALM" "clients/$TEST_CLIENT_ID/roles" -s name=user -s 'description=Admin role'
Expand All @@ -54,11 +73,12 @@ kcadm.sh create -r "$TEST_REALM" "clients/$TEST_CLIENT_ID/roles" -s name=user -s

TEST_USER_ID=$(kcadm.sh create users -r "$TEST_REALM" -s "username=$TEST_USER" -s "firstName=$TEST_USER" -s "lastName=$TEST_USER" -s "email=$TEST_USER_MAIL" -s emailVerified=true -s enabled=true -i)
kcadm.sh update -r "$TEST_REALM" "users/$TEST_USER_ID/reset-password" -s type=password -s "value=$TEST_USER_PASSWORD" -n
kcadm.sh update -r "$TEST_REALM" "users/$TEST_USER_ID/groups/$TEST_GROUP_ID"
kcadm.sh add-roles -r "$TEST_REALM" --uusername "$TEST_USER" --cid "$TEST_CLIENT_ID" --rolename admin


TEST_USER2_ID=$(kcadm.sh create users -r "$TEST_REALM" -s "username=$TEST_USER2" -s "firstName=$TEST_USER2" -s "lastName=$TEST_USER2" -s "email=$TEST_USER2_MAIL" -s emailVerified=true -s enabled=true -i)
kcadm.sh update users/$TEST_USER2_ID/reset-password -r "$TEST_REALM" -s type=password -s "value=$TEST_USER2_PASSWORD" -n
kcadm.sh update -r "$TEST_REALM" "users/$TEST_USER2_ID/groups/$TEST_GROUP_ID"
kcadm.sh add-roles -r "$TEST_REALM" --uusername "$TEST_USER2" --cid "$TEST_CLIENT_ID" --rolename user

TEST_USER3_ID=$(kcadm.sh create users -r "$TEST_REALM" -s "username=$TEST_USER3" -s "firstName=$TEST_USER3" -s "lastName=$TEST_USER3" -s "email=$TEST_USER3_MAIL" -s emailVerified=true -s enabled=true -i)
Expand Down
10 changes: 10 additions & 0 deletions playwright/compose/keycloak_setup.dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
FROM registry.access.redhat.com/ubi9 AS ubi-micro-build

RUN dnf install -y wget && wget -O /root/jq https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-linux-amd64 && chmod +x /root/jq

FROM quay.io/keycloak/keycloak:25.0.1
COPY --from=ubi-micro-build /root/jq /usr/bin/jq

COPY keycloak_setup.sh /keycloak_setup.sh

ENTRYPOINT [ "bash", "-c", "/keycloak_setup.sh"]
57 changes: 57 additions & 0 deletions playwright/tests/sso_groups.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { test, expect, type TestInfo } from '@playwright/test';

import * as utils from "../global-utils";
import { logNewUser, logUser } from './setups/sso';

let users = utils.loadEnv();

test.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => {
await utils.startVaultwarden(browser, testInfo, {
SSO_ENABLED: true,
SSO_ONLY: true,
SSO_ROLES_ENABLED: true,
SSO_ROLES_DEFAULT_TO_USER: false,
SSO_SCOPES: "email profile roles",
SSO_FRONTEND: "override",
});
});

test.afterAll('Teardown', async ({}) => {
utils.stopVaultwarden();
});

test('admin have access to vault/admin page', async ({ page }) => {
await logNewUser(test, page, users.user1, { override: true });

await page.goto('/admin');

await expect(page.getByRole('heading', { name: 'Configuration' })).toBeVisible();
});

test('user have access to vault', async ({ page }) => {
await logNewUser(test, page, users.user2, { override: true });

await page.goto('/admin');

await expect(page.getByRole('heading', { name: 'You do not have access' })).toBeVisible();
});

test('No role cannot log', async ({ page }) => {

await test.step('Landing page', async () => {
await page.goto('/');
await page.getByRole('button', { name: 'Log in'}).click();
});

await test.step('Keycloak login', async () => {
await expect(page.getByRole('heading', { name: 'Sign in to your account' })).toBeVisible();
await page.getByLabel(/Username/).fill(users.user3.name);
await page.getByLabel('Password', { exact: true }).fill(users.user3.password);
await page.getByRole('button', { name: 'Sign In' }).click();
});

await test.step('Auth failed', async () => {
await expect(page).toHaveTitle('Vaultwarden Web');
await expect(page.getByTestId("toast-message")).toHaveText(/Invalid user role/);
});
});
6 changes: 1 addition & 5 deletions src/api/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,11 +188,7 @@ pub fn create_admin_cookie<'a>() -> Cookie<'a> {
}

#[post("/", data = "<data>")]
fn post_admin_login(
data: Form<LoginForm>,
cookies: &CookieJar<'_>,
ip: ClientIp,
) -> Result<Redirect, AdminResponse> {
fn post_admin_login(data: Form<LoginForm>, cookies: &CookieJar<'_>, ip: ClientIp) -> Result<Redirect, AdminResponse> {
let data = data.into_inner();
let redirect = data.redirect;

Expand Down
3 changes: 3 additions & 0 deletions src/api/core/accounts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,9 @@ async fn post_set_password(data: Json<SetPasswordData>, headers: Headers, mut co

if CONFIG.mail_enabled() {
mail::send_set_password(&user.email.to_lowercase(), &user.name).await?;
} else if CONFIG.sso_enabled() {
// Since the user now has a password we can confirm invitations.
UserOrganization::confirm_user_invitations(&user.uuid, &mut conn).await?;
}

user.save(&mut conn).await?;
Expand Down
2 changes: 1 addition & 1 deletion src/api/core/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ mod ciphers;
mod emergency_access;
mod events;
mod folders;
mod organizations;
pub mod organizations;
mod public;
mod sends;
pub mod two_factor;
Expand Down
Loading

0 comments on commit 572c94c

Please sign in to comment.