Skip to content

Commit 8793a1a

Browse files
committed
Adding OAuth autentication support
1 parent d081308 commit 8793a1a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

70 files changed

+2894
-1921
lines changed

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ ENV RDIFFWEB_SERVER_HOST=0.0.0.0
1212

1313
RUN set -x; \
1414
apt -y update && \
15-
apt install -y --no-install-recommends librsync-dev python3-pyxattr python3-pylibacl && \
15+
apt install -y --no-install-recommends librsync-dev python3-pylibacl python3-pyxattr && \
1616
rm -Rf /var/lib/apt/lists/*
1717

1818
COPY . /src/

debian/control

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,13 @@ Build-Depends:
2424
python3-jinja2,
2525
python3-ldap3,
2626
python3-nose,
27+
python3-oauthlib,
2728
python3-parameterized,
2829
python3-pip,
2930
python3-psutil,
3031
python3-pytest,
3132
python3-requests,
33+
python3-requests-oauthlib,
3234
python3-responses,
3335
python3-selenium,
3436
python3-setuptools,

doc/configuration-ldap.md

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,22 +37,31 @@ The following settings are available for basic LDAP integration with Rdiffweb:
3737
| --ldap-base-dn (Required) | This setting specifies the DN of the branch of the directory where all searches should start from. For example: dc=my,dc=domain |
3838
| --ldap-bind-dn | This setting specifies the DN used to bind to the server when searching for entries. If not provided, Rdiffweb will use an anonymous bind. |
3939
| --ldap-bind-password | This setting specifies the password to use in conjunction with LdapBindDn. Note that the bind password is probably sensitive data and should be properly protected. You should only use the LdapBindDn and LdapBindPassword if you absolutely need them to search the directory. |
40-
| --ldap-add-missing-user | This setting enables the creation of users from LDAP when the credentials are valid. |
4140
| --ldap-username-attribute | This setting specifies the attribute to search for the username. If no attributes are provided, the default is to use uid. It's a good idea to choose an attribute that will be unique across all entries in the subtree you will be using. |
4241
| --ldap-user-filter | This setting specifies the search filter to limit LDAP lookup. If not provided, defaults to (objectClass=*), which searches for all objects in the tree. |
4342

44-
## Attribute Configuration Settings
43+
## Create or update user from LDAP
4544

46-
The following settings allow you to specify LDAP attributes for user display names, email addresses, and other attributes:
45+
When LDAP integration is enabled, the application can automatically create users in the internal database upon their first successful login. If this feature is disabled, users must be created manually before they can log in successfully.
46+
47+
### Enabling automatic user creation
48+
49+
To enable automatic user creation from LDAP, use the `--add-missing-user` option. When enabled, you can control how new users are created using additional configuration options.
50+
51+
### Configuration options
52+
53+
The following settings control user creation and attribute mapping from LDAP:
54+
55+
| Option | Description | Example |
56+
| --- | --- | --- |
57+
| `--add-missing-user` | Enables automatic creation of users from LDAP when authentication is successful. | `--add-missing-user` |
58+
| `--add-user-default-role` | Specifies the default role assigned to users created from LDAP. Only effective when `--add-missing-user` is enabled. | `--add-user-default-role=user` |
59+
| `--add-user-default-userroot` | Specifies the default root directory for users created from LDAP. Supports LDAP attribute substitution using `{attribute}` syntax. Only effective when `--add-missing-user` is enabled. | `--add-user-default-userroot=/backups/{uid}/` |
60+
| `--ldap-fullname-attribute` | LDAP attribute containing the user's full display name. If not found or empty, the full name is constructed from first name and last name attributes. | `--ldap-fullname-attribute=displayName` |
61+
| `--ldap-firstname-attribute` | LDAP attribute containing the user's first name. Used as fallback when full name attribute is not available. | `--ldap-firstname-attribute=givenName` |
62+
| `--ldap-lastname-attribute` | LDAP attribute containing the user's last name. Used as fallback when full name attribute is not available. | `--ldap-lastname-attribute=sn` |
63+
| `--ldap-email-attribute` | LDAP attribute containing the user's email address. | `--ldap-email-attribute=userPrincipalName` |
4764

48-
| Option | Description |
49-
| --- | --- |
50-
| --ldap-add-user-default-role | This setting specifies the default role used when creating users from LDAP. This parameter is only useful when --ldap-add-missing-user is enabled. |
51-
| --ldap-add-user-default-userroot | This setting specifies the default user root directory used when creating users from LDAP. LDAP attributes may be used to define the default location. For example: /backups/{uid[0]}/. This parameter is only useful when --ldap-add-missing-user is enabled. |
52-
| --ldap-fullname-attribute | This setting specifies the LDAP attribute for the user's display name. If fullname is blank, the full name is taken from the firstname and lastname. Common attributes for full names include 'cn' or 'displayName'. |
53-
| --ldap-firstname-attribute | This setting specifies the LDAP attribute for the user's first name. Used when the attribute configured for name does not exist. For example: givenName |
54-
| --ldap-lastname-attribute | This setting specifies the LDAP attribute for the user's last name. Used when the attribute configured for name does not exist. For example: sn |
55-
| --ldap-email-attribute | This setting specifies the LDAP attribute for the user's email address. For example: mail, email, userPrincipalName |
5665

5766
## Configure LDAP Group Access
5867

doc/configuration-oauth.md

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# Configuring OAuth Integration in Rdiffweb
2+
3+
This documentation guides you through configuring OAuth integration in Rdiffweb. OAuth is an authorization framework that allows third-party applications to obtain limited access to user accounts on an HTTP service. Rdiffweb is a web-based interface for managing file and directory backups. Integrating OAuth with Rdiffweb enables seamless authentication using external identity providers within your organization.
4+
5+
This integration works with most OAuth/OpenID-compliant providers, including:
6+
7+
- Google
8+
- GitLab
9+
- Auth0
10+
- and others
11+
12+
## Basic Configuration Settings
13+
14+
Open the Rdiffweb configuration file (`/etc/rdiffweb/rdw.conf`) and add the following lines:
15+
16+
```ini
17+
oauth_client_id = <OAUTH_CLIENT_ID>
18+
oauth_client_secret = <OAUTH_CLIENT_SECRET>
19+
oauth_auth_url = <OAUTH_AUTH_URL>
20+
oauth_token_url = <OAUTH_TOKEN_URL>
21+
oauth_userinfo_url = <OAUTH_USERINFO_URL>
22+
oauth_scope = <OAUTH_SCOPE>
23+
```
24+
25+
Replace the placeholders with the appropriate values from your OAuth provider.
26+
27+
The following settings are available for OAuth integration with Rdiffweb:
28+
29+
| Option | Description |
30+
|-------------------------------|-------------|
31+
| `--oauth-client-id` (Required) | The client ID provided by your OAuth provider when you register your application. Example: `abc123-def456-ghi789` |
32+
| `--oauth-client-secret` (Required) | The client secret provided by your OAuth provider. This is sensitive data and should be properly protected. |
33+
| `--oauth-auth-url` (Required) | The authorization endpoint URL of your OAuth provider. Example: `https://accounts.google.com/o/oauth2/v2/auth` |
34+
| `--oauth-token-url` (Required) | The token endpoint URL for obtaining access tokens. Example: `https://oauth2.googleapis.com/token` |
35+
| `--oauth-userinfo-url` (Required) | The user information endpoint URL for retrieving user profile data. Example: `https://www.googleapis.com/oauth2/v2/userinfo` |
36+
| `--oauth-scope` | The OAuth scopes to request. Common scopes include `openid`, `profile`, and `email`. Multiple scopes should be space-separated. Default: `openid profile email` |
37+
38+
## Creating or Updating Users from OAuth
39+
40+
When OAuth integration is enabled, Rdiffweb can automatically create users in the internal database upon their first successful login. To use this feature, ensure you have a custom and trusted OAuth provider. If this feature is disabled, users must be created manually before they can log in.
41+
42+
### Enabling Automatic User Creation
43+
44+
To enable automatic user creation, use the `--add-missing-user` option. Additional options let you control the creation process.
45+
46+
### Configuration Options
47+
48+
The following settings control user creation and attribute mapping from OAuth claims:
49+
50+
| Option | Description | Example |
51+
|--------|-------------|---------|
52+
| `--add-missing-user` | Enables automatic user creation upon successful OAuth authentication. | `--add-missing-user` |
53+
| `--add-user-default-role` | Specifies the default role for users created from OAuth. Only effective with `--add-missing-user` enabled. | `--add-user-default-role=user` |
54+
| `--add-user-default-userroot` | Specifies the default root directory for new users. Supports OAuth claim substitution via `{claim}` syntax. Only effective with `--add-missing-user`. | `--add-user-default-userroot=/backups/{sub}/` |
55+
| `--oauth-fullname-claim` | Specifies the OAuth claim for the user's full display name. If unavailable, constructs the name from first and last name claims. | `--oauth-fullname-claim=name` |
56+
| `--oauth-firstname-claim` | Specifies the OAuth claim for the user's first name (fallback). | `--oauth-firstname-claim=given_name` |
57+
| `--oauth-lastname-claim` | Specifies the OAuth claim for the user's last name (fallback). | `--oauth-lastname-claim=family_name` |
58+
| `--oauth-email-claim` | Specifies the OAuth claim for the user's email address. | `--oauth-email-claim=email` |
59+
| `--oauth-userkey-claim` | Specifies the OAuth claim to use as the username. Defaults to the email claim if unspecified. | `--oauth-userkey-claim=email` |
60+
| `--oauth-required-claim` | Requires a specific OAuth claim to have a defined value, formatted as `<claim> <value>`. | `--oauth-required-claim=email_verified true` |
61+
62+
## Example OAuth Configurations
63+
64+
### Private OAuth Provider with Automatic User Creation
65+
66+
For private OAuth providers (e.g., Auth0), it is expected that your organization manages user accounts similarly to an LDAP directory. In this scenario, configure Rdiffweb to automatically create users upon first login.
67+
68+
Ensure you have a unique claim to use as the account username (`oauth-userkey-claim`).
69+
70+
```ini
71+
oauth-client-id = your-client-id
72+
oauth-client-secret = your-client-secret
73+
oauth-auth-url = https://changeme.us.auth0.com/authorize
74+
oauth-token-url = https://changeme.us.auth0.com/oauth/token
75+
oauth-userinfo-url = https://changeme.us.auth0.com/userinfo
76+
oauth-scope = openid profile email
77+
oauth-userkey-claim = preferred_username
78+
79+
add-missing-user = True
80+
login-with-email = True
81+
```
82+
83+
### Public OAuth with Email Verification
84+
85+
If using a public provider such as Google Accounts, pre-populate all users allowed to access the application. Let them authenticate with their Google account. In this case, define `oauth-required-claim` like `email_verified true`, and set `oauth-userkey-claim` to `email` to associate accounts by email address.
86+
87+
```ini
88+
oauth-client-id = your-client-id
89+
oauth-client-secret = your-client-secret
90+
oauth-auth-url = https://accounts.google.com/o/oauth2/v2/auth
91+
oauth-token-url = https://oauth2.googleapis.com/token
92+
oauth-userinfo-url = https://openidconnect.googleapis.com/v1/userinfo
93+
oauth-scope = openid profile email
94+
oauth-required-claim = email_verified true
95+
oauth-userkey-claim = email
96+
97+
add-missing-user = False
98+
login-with-email = True
99+
```

doc/configuration.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,17 @@ titlesonly: true
123123
configuration-ldap
124124
```
125125

126+
## Configure OAuth
127+
128+
Rdiffweb may also integrate with OAuth provider to support user authentication.
129+
130+
```{toctree}
131+
---
132+
titlesonly: true
133+
---
134+
configuration-oauth
135+
```
136+
126137
## Configure User Session
127138

128139
A user session is a sequence of request and response transactions associated with a single user. The user session is the means to track each authenticated user.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ dependencies = [
3939
"psutil>=2.1.1",
4040
"pytz",
4141
"requests",
42+
"requests_oauthlib",
4243
"sqlalchemy>=1.4,<3",
4344
"WTForms>=2.2,<4",
4445
"zxcvbn>=4.4.27"

rdiffweb/controller/__init__.py

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -71,17 +71,22 @@ def flash(message, level='info'):
7171
"""
7272
assert message
7373
assert level in ['info', 'error', 'warning', 'success']
74-
assert hasattr(cherrypy, 'session'), 'flash message requires user session'
75-
if 'flash' not in cherrypy.session: # @UndefinedVariable
76-
cherrypy.session['flash'] = [] # @UndefinedVariable
77-
flash_message = FlashMessage(str(message), level)
78-
cherrypy.session['flash'].append(flash_message)
74+
session = cherrypy.serving.session
75+
if 'flash' not in session:
76+
session['flash'] = []
77+
# Support Markup and string
78+
if hasattr(message, '__html__'):
79+
flash_message = FlashMessage(message, level)
80+
else:
81+
flash_message = FlashMessage(str(message), level)
82+
session['flash'].append(flash_message)
7983

8084

8185
def get_flashed_messages():
82-
if 'flash' in cherrypy.session: # @UndefinedVariable
83-
messages = cherrypy.session['flash'] # @UndefinedVariable
84-
del cherrypy.session['flash'] # @UndefinedVariable
86+
session = cherrypy.serving.session
87+
if 'flash' in session:
88+
messages = session['flash']
89+
del session['flash']
8590
return messages
8691
return []
8792

@@ -114,13 +119,14 @@ def _compile_template(self, template_name, **kwargs):
114119
"get_flashed_messages": get_flashed_messages,
115120
"cache_invalid": self._cache_invalid,
116121
}
117-
if app.currentuser:
122+
currentuser = getattr(cherrypy.request, 'currentuser', None)
123+
if currentuser:
118124
parms.update(
119125
{
120-
'username': app.currentuser.username,
121-
'fullname': app.currentuser.fullname,
122-
'is_admin': app.currentuser.is_admin,
123-
'is_maintainer': app.currentuser.is_maintainer,
126+
'username': currentuser.username,
127+
'fullname': currentuser.fullname,
128+
'is_admin': currentuser.is_admin,
129+
'is_maintainer': currentuser.is_maintainer,
124130
}
125131
)
126132
elif getattr(cherrypy.serving.request, 'login', None):

rdiffweb/controller/api.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,16 +43,16 @@ def _checkpassword(realm, username, password):
4343
return True
4444
# Disable password authentication for MFA
4545
if userobj.mfa == UserObject.ENABLED_MFA:
46-
cherrypy.tools.ratelimit.hit()
46+
cherrypy.tools.ratelimit.increase_hit()
4747
return False
4848
# Otherwise validate username password
49-
valid = any(cherrypy.engine.publish('login', username, password))
49+
valid = cherrypy.tools.auth.login_with_credentials(username, password)
5050
if valid:
5151
# Store scope
5252
cherrypy.serving.request.scope = ['all']
5353
return True
5454
# When invalid, we need to increase the rate limit.
55-
cherrypy.tools.ratelimit.hit()
55+
cherrypy.tools.ratelimit.increase_hit()
5656
return False
5757

5858

@@ -61,7 +61,7 @@ def _checkpassword(realm, username, password):
6161
@cherrypy.tools.json_out(on=True)
6262
@cherrypy.tools.json_in(on=True, force=False)
6363
@cherrypy.tools.auth_basic(realm='rdiffweb', checkpassword=_checkpassword, priority=70)
64-
@cherrypy.tools.auth_form(on=False)
64+
@cherrypy.tools.auth(on=True, redirect=False)
6565
@cherrypy.tools.auth_mfa(on=False)
6666
@cherrypy.tools.i18n(on=False)
6767
@cherrypy.tools.ratelimit(scope='rdiffweb-api', hit=0, priority=69)

rdiffweb/controller/api_currentuser.py

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from wtforms.validators import Length, Optional, Regexp
2020

2121
from rdiffweb.controller import Controller
22-
from rdiffweb.controller.form import CherryForm
22+
from rdiffweb.controller.formdb import DbForm
2323
from rdiffweb.controller.page_pref_sshkeys import ApiSshKeys
2424
from rdiffweb.controller.page_pref_tokens import ApiTokens
2525
from rdiffweb.controller.page_settings import ApiRepos
@@ -33,7 +33,7 @@
3333
from wtforms.fields.html5 import EmailField # wtform <3
3434

3535

36-
class CurrentUserForm(CherryForm):
36+
class CurrentUserForm(DbForm):
3737
"""
3838
Form used to validate input data for REST api request.
3939
"""
@@ -76,7 +76,6 @@ def populate_obj(self, user):
7676
user.email = self.email.data
7777
user.lang = self.lang.data
7878
user.report_time_range = self.report_time_range.data
79-
user.add()
8079

8180

8281
@cherrypy.expose
@@ -130,7 +129,7 @@ def get(self):
130129
- `encoding`: The encoding used for the repository.
131130
132131
"""
133-
u = self.app.currentuser
132+
u = cherrypy.serving.request.currentuser
134133
if u.refresh_repos():
135134
u.commit()
136135
return {
@@ -171,14 +170,11 @@ def post(self, **kwargs):
171170
Returns status 200 OK on success.
172171
"""
173172
# Validate input data.
174-
userobj = self.app.currentuser
173+
userobj = cherrypy.serving.request.currentuser
175174
form = CurrentUserForm(obj=userobj, json=1)
176175
if not form.strict_validate():
177176
raise cherrypy.HTTPError(400, form.error_message)
178177
# Apply changes
179-
try:
180-
form.populate_obj(userobj)
181-
userobj.commit()
182-
except Exception as e:
183-
userobj.rollback()
184-
raise cherrypy.HTTPError(400, str(e))
178+
if form.save_to_db(userobj):
179+
return None
180+
raise cherrypy.HTTPError(400, form.error_message)

rdiffweb/controller/dispatch.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
import cherrypy
2424

25-
import rdiffweb.tools.auth_form # noqa
25+
import rdiffweb.tools.auth # noqa
2626
import rdiffweb.tools.auth_mfa # noqa
2727
import rdiffweb.tools.ratelimit # noqa
2828

@@ -33,7 +33,7 @@ def staticdir(path, doc=''):
3333
"""
3434

3535
@cherrypy.expose
36-
@cherrypy.tools.auth_form(on=False)
36+
@cherrypy.tools.auth(on=False)
3737
@cherrypy.tools.auth_mfa(on=False)
3838
@cherrypy.tools.ratelimit(on=False)
3939
@cherrypy.tools.sessions(on=False)
@@ -53,7 +53,7 @@ def staticfile(path, doc=''):
5353
"""
5454

5555
@cherrypy.expose
56-
@cherrypy.tools.auth_form(on=False)
56+
@cherrypy.tools.auth(on=False)
5757
@cherrypy.tools.auth_mfa(on=False)
5858
@cherrypy.tools.ratelimit(on=False)
5959
@cherrypy.tools.sessions(on=False)

0 commit comments

Comments
 (0)