Skip to content

Commit

Permalink
Merge pull request #1708 from pbiering/merge-pam-auth-from-v1
Browse files Browse the repository at this point in the history
Merge pam auth from v1
  • Loading branch information
pbiering authored Feb 22, 2025
2 parents 970d4ba + 6518f1b commit 5302863
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 1 deletion.
16 changes: 16 additions & 0 deletions DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -829,6 +829,10 @@ Available backends:
`oauth2`
: Use an OAuth2 server to authenticate users.

`pam`
: Use local PAM to authenticate users.


Default: `none`

##### cache_logins
Expand Down Expand Up @@ -1028,6 +1032,18 @@ OAuth2 token endpoint URL

Default:

##### pam_service

PAM service

Default: radicale

##### pam_group_membership

PAM group user should be member of

Default:

##### lc_username

Сonvert username to lowercase, must be true for case-insensitive auth
Expand Down
8 changes: 7 additions & 1 deletion config
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
[auth]

# Authentication method
# Value: none | htpasswd | remote_user | http_x_remote_user | dovecot | ldap | oauth2 | denyall
# Value: none | htpasswd | remote_user | http_x_remote_user | dovecot | ldap | oauth2 | pam | denyall
#type = none

# Cache logins for until expiration time
Expand Down Expand Up @@ -128,6 +128,12 @@
# OAuth2 token endpoint URL
#oauth2_token_endpoint = <URL>

# PAM service
#pam_serivce = radicale

# PAM group user should be member of
#pam_group_membership =

# Htpasswd filename
#htpasswd_filename = /etc/radicale/users

Expand Down
2 changes: 2 additions & 0 deletions radicale/auth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"ldap",
"imap",
"oauth2",
"pam",
"dovecot")

CACHE_LOGIN_TYPES: Sequence[str] = (
Expand All @@ -51,6 +52,7 @@
"htpasswd",
"imap",
"oauth2",
"pam",
)

AUTH_SOCKET_FAMILY: Sequence[str] = ("AF_UNIX", "AF_INET", "AF_INET6")
Expand Down
105 changes: 105 additions & 0 deletions radicale/auth/pam.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# -*- coding: utf-8 -*-
#
# This file is part of Radicale Server - Calendar Server
# Copyright © 2011 Henry-Nicolas Tourneur
# Copyright © 2021-2021 Unrud <[email protected]>
# Copyright © 2025-2025 Peter Bieringer <[email protected]>
#
# This library is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.

"""
PAM authentication.
Authentication using the ``pam-python`` module.
Important: radicale user need access to /etc/shadow by e.g.
chgrp radicale /etc/shadow
chmod g+r
"""

import grp
import pwd

from radicale import auth
from radicale.log import logger


class Auth(auth.BaseAuth):
def __init__(self, configuration) -> None:
super().__init__(configuration)
try:
import pam
self.pam = pam
except ImportError as e:
raise RuntimeError("PAM authentication requires the Python pam module") from e
self._service = configuration.get("auth", "pam_service")
logger.info("auth.pam_service: %s" % self._service)
self._group_membership = configuration.get("auth", "pam_group_membership")
if (self._group_membership):
logger.info("auth.pam_group_membership: %s" % self._group_membership)
else:
logger.info("auth.pam_group_membership: (empty, nothing to check / INSECURE)")

def pam_authenticate(self, *args, **kwargs):
return self.pam.authenticate(*args, **kwargs)

def _login(self, login: str, password: str) -> str:
"""Check if ``user``/``password`` couple is valid."""
if login is None or password is None:
return ""

# Check whether the user exists in the PAM system
try:
pwd.getpwnam(login).pw_uid
except KeyError:
logger.debug("PAM user not found: %r" % login)
return ""
else:
logger.debug("PAM user found: %r" % login)

# Check whether the user has a primary group (mandatory)
try:
# Get user primary group
primary_group = grp.getgrgid(pwd.getpwnam(login).pw_gid).gr_name
logger.debug("PAM user %r has primary group: %r" % (login, primary_group))
except KeyError:
logger.debug("PAM user has no primary group: %r" % login)
return ""

# Obtain supplementary groups
members = []
if (self._group_membership):
try:
members = grp.getgrnam(self._group_membership).gr_mem
except KeyError:
logger.debug(
"PAM membership required group doesn't exist: %r" %
self._group_membership)
return ""

# Check whether the user belongs to the required group
# (primary or supplementary)
if (self._group_membership):
if (primary_group != self._group_membership) and (login not in members):
logger.warning("PAM user %r belongs not to the required group: %r" % (login, self._group_membership))
return ""
else:
logger.debug("PAM user %r belongs to the required group: %r" % (login, self._group_membership))

# Check the password
if self.pam_authenticate(login, password, service=self._service):
return login
else:
logger.debug("PAM authentication not successful for user: %r (service %r)" % (login, self._service))
return ""
8 changes: 8 additions & 0 deletions radicale/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,14 @@ def json_str(value: Any) -> dict:
"value": "",
"help": "OAuth2 token endpoint URL",
"type": str}),
("pam_group_membership", {
"value": "",
"help": "PAM group user should be member of",
"type": str}),
("pam_service", {
"value": "radicale",
"help": "PAM service",
"type": str}),
("strip_domain", {
"value": "False",
"help": "strip domain from username",
Expand Down
2 changes: 2 additions & 0 deletions radicale/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.

import ssl
import sys
from importlib import import_module, metadata
from typing import Callable, Sequence, Type, TypeVar, Union

Expand Down Expand Up @@ -55,6 +56,7 @@ def package_version(name):

def packages_version():
versions = []
versions.append("python=%s.%s.%s" % (sys.version_info[0], sys.version_info[1], sys.version_info[2]))
for pkg in RADICALE_MODULES:
versions.append("%s=%s" % (pkg, package_version(pkg)))
return " ".join(versions)
Expand Down

0 comments on commit 5302863

Please sign in to comment.