Skip to content
Snippets Groups Projects
Verified Commit c485b7c0 authored by Nik | Klampfradler's avatar Nik | Klampfradler
Browse files

Rewrite in Rust

parent 42e7273e
No related branches found
No related tags found
No related merge requests found
......@@ -6,6 +6,6 @@ end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.py]
[*.rs]
indent_style = space
indent_size = 4
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# Cython debug symbols
cython_debug/
# These are backup files generated by rustfmt
**/*.rs.bk
# Editor files
*~
......
[tool.poetry]
name = "nss-pam-oidc"
[package]
name = "nss_pam_oidc"
version = "0.1.0"
description = "NSS/PAM modules for OpenID Connect/OAuth2"
authors = ["Dominik George <nik@naturalnet.de>"]
license = "Apache-2.0"
readme = "README.rst"
edition = "2018"
description = "NSS/PAM modules for OpenID Connect/OAuth2"
repository = "https://edugit.org/debian-edu-ng/nss-pam-oidc"
classifiers = [
"Environment :: Console",
"Intended Audience :: System Administrators",
]
license = "Apache-2.0"
categories = ["authentication", "os", "os::linux-apis"]
[tool.poetry.dependencies]
python = "^3.9"
toml = "^0.10.2"
oauthlib = "^3.1.0"
requests-oauthlib = "^1.3.0"
[lib]
name = "nss_pam_oidc"
crate-type = [ "cdylib" ]
[tool.poetry.dev-dependencies]
[dependencies]
pamsm = { version = "^0.4.2", features = ["libpam"] }
oauth2 = "^4.0.0"
config = "^0.11.0"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[profile.release]
opt-level = 'z'
lto = true
# Copyright 2021 Dominik George <nik@naturalnet.de>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from nss_pam_oidc.pam import (
pam_sm_authenticate,
pam_sm_setcred,
pam_sm_acct_mgmt,
pam_sm_open_session,
pam_sm_close_session,
pam_sm_chauthtok,
)
# Copyright 2021 Dominik George <nik@naturalnet.de>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import toml
DEFAULT_CONFIG_FILE = "/etc/nss_pam_oidc.toml"
DEFAULT_CONFIG = {
"pam": {
"flow": "password",
},
"nss": {},
}
def load_config(config_file: str) -> dict[str, dict[str, str]]:
"""Load full configuration, amended wit hdefault config."""
config = {}
# First, copy default configuration
for name, conf in DEFAULT_CONFIG.items():
config[name] = conf.copy()
# Second, load configuration from file
if os.path.exists(config_file):
config_toml = toml.load(config_file)
for name, conf in config.items():
conf.update(config_toml.get(name, {}))
return config
def get_config(section: str, **kwargs) -> dict[str, str]:
"""Get configuration for a section, layered from defaults, config file, and args."""
config_file = kwargs.pop("config", DEFAULT_CONFIG_FILE)
config = load_config(config_file)
# Lastly, override with passed arguments
config[section].update(kwargs)
return config[section]
def filter_config(config: dict[str, str], keys: list[str]) -> dict[str, str]:
"""Return a copy of the config dictionary with only the requested keys."""
return {k: v for k, v in config.items() if k in keys}
# Copyright 2021 Dominik George <nik@naturalnet.de>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from oauthlib.oauth2 import InvalidGrantError, LegacyApplicationClient
from requests.exceptions import RequestException
from requests_oauthlib import OAuth2Session
from .config import filter_config, get_config
def _split_argv(argv: list[str]) -> dict[str, str]:
args = {}
for arg in argv:
if "=" in arg:
name, val = arg.split("=", 1)
else:
name, val = arg, True
args[name] = val
return args
def _do_legacy_auth(username: str, password: str, config: dict[str, str]):
client_config = filter_config(config, ["client_id"])
session_config = filter_config(config, ["client_id"])
fetch_config = filter_config(config, ["client_id", "client_secret", "token_url"])
client = LegacyApplicationClient(**client_config)
session = OAuth2Session(client=client, **session_config)
token = session.fetch_token(username=username, password=password, **fetch_config)
return token
def pam_sm_authenticate(pamh, flags, argv):
args = _split_argv(argv)
config = get_config("pam", args)
if config.pop("flow") == "password":
try:
user = pamh.get_user(None)
password = pamh.authtok
except pamh.exception as e:
return e.pam_result
if user is None or password is None:
return pamh.PAM_CRED_INSUFFICIENT
try:
token = _do_legacy_auth(username, password)
except InvalidGrantError:
return pamh.PAM_AUTH_ERROR
except RequestException:
return pamh.PAM_AUTHINFO_UNAVAIL
except:
return pamh.PAM_SERVICE_ERR
if "access_token" in token:
return pamh.PAM_SUCCESS
return pamh.PAM_AUTH_ERR
def pam_sm_setcred(pamh, flags, argv):
return pamh.PAM_SUCCESS
def pam_sm_acct_mgmt(pamh, flags, argv):
return pamh.PAM_SUCCESS
def pam_sm_open_session(pamh, flags, argv):
return pamh.PAM_SUCCESS
def pam_sm_close_session(pamh, flags, argv):
return pamh.PAM_SUCCESS
def pam_sm_chauthtok(pamh, flags, argv):
return pamh.PAM_SUCCESS
[[package]]
name = "certifi"
version = "2020.12.5"
description = "Python package for providing Mozilla's CA Bundle."
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "chardet"
version = "4.0.0"
description = "Universal encoding detector for Python 2 and 3"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "idna"
version = "2.10"
description = "Internationalized Domain Names in Applications (IDNA)"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "oauthlib"
version = "3.1.0"
description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[package.extras]
rsa = ["cryptography"]
signals = ["blinker"]
signedtoken = ["cryptography", "pyjwt (>=1.0.0)"]
[[package]]
name = "requests"
version = "2.25.1"
description = "Python HTTP for Humans."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[package.dependencies]
certifi = ">=2017.4.17"
chardet = ">=3.0.2,<5"
idna = ">=2.5,<3"
urllib3 = ">=1.21.1,<1.27"
[package.extras]
security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"]
socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
[[package]]
name = "requests-oauthlib"
version = "1.3.0"
description = "OAuthlib authentication support for Requests."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[package.dependencies]
oauthlib = ">=3.0.0"
requests = ">=2.0.0"
[package.extras]
rsa = ["oauthlib[signedtoken] (>=3.0.0)"]
[[package]]
name = "toml"
version = "0.10.2"
description = "Python Library for Tom's Obvious, Minimal Language"
category = "main"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "urllib3"
version = "1.26.4"
description = "HTTP library with thread-safe connection pooling, file post, and more."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
[package.extras]
secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
brotli = ["brotlipy (>=0.6.0)"]
[metadata]
lock-version = "1.1"
python-versions = "^3.9"
content-hash = "f70d776e9a6a2676f9c1816a08a35616c61a5a9af75631200da033c3afd4cb4e"
[metadata.files]
certifi = [
{file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"},
{file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"},
]
chardet = [
{file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"},
{file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"},
]
idna = [
{file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"},
{file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"},
]
oauthlib = [
{file = "oauthlib-3.1.0-py2.py3-none-any.whl", hash = "sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea"},
{file = "oauthlib-3.1.0.tar.gz", hash = "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889"},
]
requests = [
{file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"},
{file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"},
]
requests-oauthlib = [
{file = "requests-oauthlib-1.3.0.tar.gz", hash = "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a"},
{file = "requests_oauthlib-1.3.0-py2.py3-none-any.whl", hash = "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d"},
{file = "requests_oauthlib-1.3.0-py3.7.egg", hash = "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc"},
]
toml = [
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
]
urllib3 = [
{file = "urllib3-1.26.4-py2.py3-none-any.whl", hash = "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df"},
{file = "urllib3-1.26.4.tar.gz", hash = "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937"},
]
/* Copyright 2021 Dominik George <nik@naturalnet.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
extern crate config;
fn load_config(config_file: String) -> config::Config {
let mut conf = config::Config::default();
conf.set("pam.flow", "password");
conf
.merge(config::File::with_name(&config_file)).unwrap()
.merge(config::Environment::with_prefix("NSS_PAM_OIDC")).unwrap();
return conf;
}
pub fn get_config(conf_args: config::Config) -> config::Config {
let config_file: String;
let config_file_passed = conf_args.get_str("config");
if config_file_passed.is_ok() {
config_file = config_file_passed.unwrap().to_string();
} else {
config_file = "/etc/nss_pam_oidc".to_string();
}
let mut conf = load_config(config_file);
conf.merge(conf_args).unwrap();
return conf;
}
pub fn argv_to_config(argv: Vec<String>) -> config::Config {
let mut conf = config::Config::default();
for arg in &argv {
if arg.contains("=") {
let split: Vec<&str> = arg.split("=").collect();
conf.set(split[0], split[1]);
} else {
conf.set(arg, true);
}
}
return conf;
}
mod config;
mod pam;
use crate::config::{
argv_to_config,
get_config
};
use config::Config;
use oauth2::{
AuthUrl,
ClientId,
ClientSecret,
ResourceOwnerPassword,
ResourceOwnerUsername,
Scope,
TokenUrl
};
use oauth2::basic::{
BasicClient,
BasicTokenResponse
};
use oauth2::reqwest::http_client;
extern crate pamsm;
use pamsm::{PamServiceModule, Pam, PamFlag, PamError, PamLibExt};
fn get_or_pam_error(config: &Config, key: &str) -> Result<String, PamError> {
match config.get_str(key) {
Ok(v) => Ok(v),
_ => Err(PamError::SERVICE_ERR),
}
}
fn do_legacy_auth(username: String, password: String, config: Config) -> Result<BasicTokenResponse, PamError> {
let client_id = ClientId::new(get_or_pam_error(&config, "pam.client_id")?);
let client_secret = ClientSecret::new(get_or_pam_error(&config, "pam.client_secret")?);
let auth_url = match AuthUrl::new(get_or_pam_error(&config, "pam.auth_url")?) {
Ok(u) => u,
_ => return Err(PamError::SERVICE_ERR),
};
let token_url = match TokenUrl::new(get_or_pam_error(&config, "pam.token_url")?){
Ok(u) => u,
_ => return Err(PamError::SERVICE_ERR),
};
let scope = get_or_pam_error(&config, "pam.scope")?;
let client = BasicClient::new(client_id, Some(client_secret), auth_url, Some(token_url));
match client
.exchange_password(
&ResourceOwnerUsername::new(username),
&ResourceOwnerPassword::new(password)
)
.add_scope(Scope::new(scope.to_string()))
.request(http_client) {
Ok(t) => Ok(t),
_ => Err(PamError::AUTHINFO_UNAVAIL),
}
}
struct PamOidc;
impl PamServiceModule for PamOidc {
fn authenticate(pamh: Pam, _: PamFlag, argv: Vec<String>) -> PamError {
let conf_args = argv_to_config(argv);
let conf = get_config(conf_args);
if conf.get_str("pam.flow").unwrap() == "password" {
let username = match pamh.get_user(None) {
Ok(Some(u)) => match u.to_str() {
Ok(u) => u,
_ => return PamError::CRED_INSUFFICIENT,
},
Ok(None) => return PamError::CRED_INSUFFICIENT,
Err(e) => return e,
};
let password = match pamh.get_authtok(None) {
Ok(Some(p)) => match p.to_str() {
Ok(p) => p,
_ => return PamError::CRED_INSUFFICIENT,
},
Ok(None) => return PamError::CRED_INSUFFICIENT,
Err(e) => return e,
};
match do_legacy_auth(username.to_string(), password.to_string(), conf) {
Ok(_) => return PamError::SUCCESS,
Err(e) => return e,
};
}
return PamError::SERVICE_ERR;
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment