Full Code of jirutka/ssh-ldap-pubkey for AI

master 7ba9cebd22d7 cached
23 files
42.3 KB
12.2k tokens
45 symbols
1 requests
Download .txt
Repository: jirutka/ssh-ldap-pubkey
Branch: master
Commit: 7ba9cebd22d7
Files: 23
Total size: 42.3 KB

Directory structure:
gitextract_5m6ot60j/

├── .editorconfig
├── .github/
│   ├── FUNDING.yml
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── CHANGELOG.adoc
├── LICENSE
├── MANIFEST.in
├── README.md
├── bin/
│   ├── ssh-ldap-pubkey
│   └── ssh-ldap-pubkey-wrapper
├── etc/
│   ├── ldap.conf
│   └── openssh-lpk.schema
├── requirements.txt
├── setup.cfg
├── setup.py
├── ssh_ldap_pubkey/
│   ├── __init__.py
│   ├── config.py
│   └── exceptions.py
└── tests/
    ├── __init__.py
    ├── config_test.py
    ├── fixtures/
    │   ├── invalid_ssh_keys
    │   └── valid_ssh_keys
    └── functions_test.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
# http://editorconfig.org/
root = true

[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true

[*.schema]
indent_style = tab

[bin/ssh-ldap-pubkey-wrapper]
indent_style = tab

[*.yml]
indent_size = 2


================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms

github: jirutka


================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on:
  - push
  - pull_request

jobs:
  test:
    name: Test on Python ${{ matrix.python }}
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        python:
          - '3.6'
          - '3.7'
          - '3.8'
          - '3.9'
          - '3.10'
    steps:
      - uses: actions/checkout@v3

      - name: Install Python ${{ matrix.python }}
        uses: actions/setup-python@v3
        with:
          python-version: ${{ matrix.python }}

      - name: Install system dependencies
        run: |
          sudo apt update -qq
          sudo apt install -q libldap-dev libsasl2-dev

      - name: Install project requirements
        run: |
          pip install -U -r requirements.txt
          python3 setup.py install

      - name: Run tests
        run: py.test --cov=ssh_ldap_pubkey --cov-report term -vv

      - name: Run linter
        run: pycodestyle

  publish:
    name: Publish to PyPI
    if: startsWith(github.ref, 'refs/tags/v') && github.event_name == 'push'
    needs: [test]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - uses: actions/setup-python@v3
        with:
          python-version: '3.10'

      - name: Build source tarball
        run: python3 setup.py sdist

      - name: Publish package to PyPI
        uses: pypa/gh-action-pypi-publish@release/v1
        with:
          user: __token__
          password: ${{ secrets.PYPI_API_TOKEN }}


================================================
FILE: .gitignore
================================================
# Byte-compiled / optimized files
__pycache__/
*.py[cod]

# Distribution / packaging
env/
build/
dist/
/MANIFEST
*.egg-info/
*.egg

# pytest
.cache/

# Coverage report
.coverage


================================================
FILE: CHANGELOG.adoc
================================================
= Changelog
:repo-uri: https://github.com/jirutka/ssh-ldap-pubkey
:issues: {repo-uri}/issues
:pulls: {repo-uri}/pull
:tags: {repo-uri}/releases/tag


== unreleased


== link:{tags}/v1.4.0[1.4.0] (2022-05-19)

* Replace deprecated pyldap with python-ldap.
* Drop support for Python 2.
* Replace `base64.decodestring()`, which has been removed in Python 3.9, with `base64.decodebytes()`. ({issues}/49[#49])


== link:{tags}/v1.3.3[1.3.3] (2020-05-15)

* Fix wrapper script to be compatible with Busybox `logger(1)`. ({issues}/43[#43])
* Fix `tls_reqcert` value of never to be accepted when defined. (PR {pulls}/47[#47])


== link:{tags}/v1.3.2[1.3.2] (2019-08-21)

* Fix broken keys listing due to over-wrapping of search filter. ({issues}/34[#34])


== link:{tags}/v1.3.1[1.3.1] (2019-04-27)

* Fix handling of complex LDAP filters. (PR {pulls}/33[#33])
* Retire Python 3.3 support (due to pyldap). (PR {pulls}/33[#33])


== link:{tags}/v1.3.0[1.3.0] (2018-03-02)

* Add support for SASL GSSAPI (Kerberos) authentication. (PR {pulls}/27[#27])
* Allow to disable LDAP referrals using option `referrals`.


== link:{tags}/v1.2.0[1.2.0] (2017-02-24)

* Make pubkey class and attribute configurable. (PR {pulls}/21[#21])


== link:{tags}/v1.1.1[1.1.1] (2017-01-04)

* Fix parsing of `uri` from config file. ({issues}/20[#20])


== link:{tags}/v1.1.0[1.1.0] (2016-12-28)

* Add support for multiple LDAP servers in ldap.conf.
* Allow to pass multiple `--uri` options.


== link:{tags}/v1.0.0[1.0.0] (2016-10-01)

* Refactor code-base, split it into a module and CLI script.
* Add support for `TLS_REQCERT` option. (PR {pulls}/11[#12])
* Add support for StartTLS. (PR {pulls}/14[#14])
* Replace python-ldap with pyldap.
* Make it compatible with Python 3. ({issues}/15[#15])
* Change sshPublicKey in ldapPublicKey objectclass to be optional.


== link:{tags}/v0.4.1[0.4.1] (2015-10-08)

* Catch `ldap.INSUFFICIENT_ACCESS` exception when adding/removing key to/from LDAP and print error message. (PR {pulls}/9[#9])


== link:{tags}/v0.4.0[0.4.0] (2015-02-07)

* Add option `-D` to specify the bind DN. ({issues}/7[#7])


== link:{tags}/v0.3.3[0.3.3] (2015-02-07)

* Fix keys count in the wrapper script to return 0 instead of 1 when no key is found. (PR {pulls}/8[#8])


== link:{tags}/v0.3.2[0.3.2] (2014-12-14)

* Remove unnecessary absolute path in wrapper script. ({issues}/6[#6])


== link:{tags}/v0.3.1[0.3.1] (2014-09-16)

* Log all info and warn messages to stderr instead of stdout.


== link:{tags}/v0.3.0[0.3.0] (2014-09-16)

* Print warnings to stderr, not stdout.
* Change script option `-h` to `-H` to avoid confusion with help.


== link:{tags}/v0.2.3[0.2.3] (2014-09-02)

* Display username in the password prompt. (PR {pulls}/3[#3])
* Fix SSH key validation to accept keys without a comment part.
* Treat keys in config file as case-insensitive (always convert them to lowercase).


== link:{tags}/v0.2.2[0.2.2] (2014-04-23)

* Add basic validation of SSH public key format.
* Check if public key doesn’t already exist in the user’s entry before adding it.


== link:{tags}/v0.2.1[0.2.1] (2014-04-22)

* Handle tls_cacertdir configuration directive.


== link:{tags}/v0.2[0.2] (2014-04-21)

* First public release.


================================================
FILE: LICENSE
================================================
The MIT License

Copyright 2014-present Jakub Jirutka <jakub@jirutka.cz>.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


================================================
FILE: MANIFEST.in
================================================
include README.md
include CHANGELOG.adoc
include LICENSE
include MANIFEST.in
include etc/ldap.conf
include etc/openssh-lpk.schema


================================================
FILE: README.md
================================================
OpenSSH / LDAP public keys
==========================
[![Build Status](https://github.com/jirutka/ssh-ldap-pubkey/workflows/CI/badge.svg)](https://github.com/jirutka/ssh-ldap-pubkey/actions?query=workflow%3A%22CI%22)
[![Code Climate](https://codeclimate.com/github/jirutka/ssh-ldap-pubkey/badges/gpa.svg)](https://codeclimate.com/github/jirutka/ssh-ldap-pubkey)
[![version](https://img.shields.io/pypi/v/ssh-ldap-pubkey.svg?style=flat)](https://pypi.python.org/pypi/ssh-ldap-pubkey)

This project provides an utility to manage SSH public keys stored in LDAP and also a script for
OpenSSH server to load authorized keys from LDAP.


Why?
----

When you have dozen of servers it becomes difficult to manage your authorized keys. You have to
copy all your public keys to `~/.ssh/authorized_keys` on every server you want to login to. And
what if you someday change your keys?

It’s a good practice to use some kind of a centralized user management, usually an LDAP server.
There you have user’s login, uid, e-mail, … and password. What if we could also store public SSH
keys on LDAP server? With this utility it’s easy as pie.


Alternatives
------------

If you need just a lightweight utility for OpenSSH server to load authorized keys from LDAP,
then you can use [ssh-getkey-ldap](https://github.com/jirutka/ssh-getkey-ldap) written in Lua
or [this one](https://gist.github.com/jirutka/b15c31b2739a4f3eab63) written in POSIX shell
(but it requires `ldapsearch` utility and may not work well on some systems).


Requirements
------------

* Python 3.6+
* [python-ldap] 3.x
* [docopt] 0.6.x

You can install both Python modules from PyPI.
python-ldap requires additional system dependencies – OpenLDAP.
Refer to [Stack Overflow](http://stackoverflow.com/q/4768446/240963) for distribution-specific information.


Installation
------------

### PyPI:

    pip install ssh-ldap-pubkey

### Alpine Linux

    apk add ssh-ldap-pubkey

Note: The package is currently in the (official) _community_ repository; make sure that you have community in `/etc/apk/repositories`.


Usage
-----

List SSH public keys stored in LDAP for the current user:

    ssh-ldap-pubkey list

List SSH public keys stored in LDAP for the specified user:

    ssh-ldap-pubkey list -u flynn

Add the specified SSH public key for the current user to LDAP:

    ssh-ldap-pubkey add ~/.ssh/id_rsa.pub

Remove SSH public key(s) of the current user that matches the specified pattern:

    ssh-ldap-pubkey del flynn@grid

Specify LDAP URI and base DN on command line instead of configuration file:

    ssh-ldap-pubkey list -b ou=People,dc=encom,dc=com -H ldaps://encom.com -u flynn

As the LDAP manager, add SSH public key to LDAP for the specified user:

    ssh-ldap-pubkey add -D cn=Manager,dc=encom,dc=com -u flynn ~/.ssh/id_rsa.pub

Show help for other options:

    ssh-ldap-pubkey --help


Configuration
-------------

Configuration is read from /etc/ldap.conf — file used by LDAP nameservice switch library and the
LDAP PAM module. An example file is included in [etc/ldap.conf][ldap.conf]. The following subset of
parameters are used:

*  **uri** ... URI(s) of the LDAP server(s) to connect to, separated by a space. The URI scheme may
               be ldap, or ldaps. Default is `ldap://localhost`.
*  **nss_base_passwd** ... distinguished name (DN) of the search base.
*  **base** ... distinguished name (DN) of the search base. Used when *nss_base_passwd* is not set.
*  **scope** ... search scope; _sub_, _one_, or _base_ (default is _sub_).
*  **referrals** ... should client automatically follow referrals returned by LDAP servers (default is _on_)?
*  **pam_filter** ... filter to use when searching for the user’s entry, additional to the login
        attribute value assertion (`pam_login_attribute=<login>`). Default is
        _objectclass=posixAccount_.
*  **pam_login_attribute** ... the user ID attribute (default is _uid_).
*  **ldap_version** ... LDAP version to use (default is 3).
*  **sasl** ... enable SASL and specify mechanism to use (currently only GSSAPI is supported).
*  **binddn** ... distinguished name (DN) to bind when reading the user’s entry (default is to bind
                  anonymously).
*  **bindpw** ... credentials to bind with when reading the user’s entry (default is none).
*  **ssl** ... LDAP SSL/TLS method; _off_, _on_, or _start_tls_. If you use LDAP over SSL (i.e. URI `ldaps://`), leave this empty.
*  **timelimit** ... search time limit in seconds (default is 10).
*  **bind_timelimit** ... bind/connect time limit in seconds (default is 10). If multiple URIs are
                          specified in _uri_, then the next one is tried after this timeout.
*  **tls_cacertdir** ... path of the directory with CA certificates for LDAP server certificate
                         verification.
*  **pubkey_class** ... objectClass that should be added/removed to/from the user’s entry when adding/removing first/last public key and the *pubkey_attr* is mandatory for this class.
   This is needed for the original openssh-lpk.schema (not for the one in this repository).
   Default is `ldapPublicKey`.
*  **pubkey_attr** ... name of LDAP attribute used for SSH public keys (default is `sshPublicKey`).

The only required parameter is *nss_base_passwd* or _base_, others have sensitive defaults. You
might want to define _uri_ parameter as well. These parameters can be also defined/overriden
with `--bind` and `--uri` options on command line.

For more information about these parameters refer to ldap.conf man page.


Set up OpenSSH server
--------------------

To configure OpenSSH server to fetch users’ authorized keys from LDAP server:

1.  Make sure that you have installed **ssh-ldap-pubkey** and **ssh-ldap-pubkey-wrapper** in
    `/usr/bin` with owner `root` and mode `0755`.
2.  Add these two lines to /etc/ssh/sshd_config:

        AuthorizedKeysCommand /usr/bin/ssh-ldap-pubkey-wrapper
        AuthorizedKeysCommandUser nobody

3.  Restart sshd and check log file if there’s no problem.

Note: This method is supported by OpenSSH since version 6.2-p1 (or 5.3 onRedHat). If you have an
older version and can’t upgrade, for whatever weird reason, use [openssh-lpk] patch instead.


Set up LDAP server
------------------

Just add the [openssh-lpk.schema] to your LDAP server, **or** add an attribute named `sshPublicKey`
to any existing schema which is already defined in people entries. That’s all.

Note: Presumably, you’ve already set up your LDAP server for centralized unix users management,
i.e. you have the [NIS schema](http://www.zytrax.com/books/ldap/ape/nis.html) and users in LDAP.


License
-------

This project is licensed under [MIT license](http://opensource.org/licenses/MIT).


[python-ldap]: https://pypi.python.org/pypi/python-ldap/
[docopt]: https://pypi.python.org/pypi/docopt/
[ebuild]: https://github.com/cvut/gentoo-overlay/tree/master/sys-auth/ssh-ldap-pubkey
[cvut-overlay]: https://github.com/cvut/gentoo-overlay
[openssh-lpk]: http://code.google.com/p/openssh-lpk/

[ldap.conf]: https://github.com/jirutka/ssh-ldap-pubkey/blob/master/etc/ldap.conf
[openssh-lpk.schema]: https://github.com/jirutka/ssh-ldap-pubkey/blob/master/etc/openssh-lpk.schema


================================================
FILE: bin/ssh-ldap-pubkey
================================================
#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
ssh-ldap-pubkey - Utility to manage SSH public keys stored in LDAP.

Usage:
  ssh-ldap-pubkey list [-H URI...] [options]
  ssh-ldap-pubkey add [-H URI...] [options] FILE
  ssh-ldap-pubkey del [-H URI...] [options] PATTERN
  ssh-ldap-pubkey --help

  -                      Read public key from stdin.
  FILE                   Path to the public key file to add.
  PATTERN                Pattern that specifies public key(s) to delete, i.e.
                         a complete key or just a part of it.

Options:
  -b DN --base=DN        Base DN where to search for the users' entry. If not
                         provided, then it's read from the config file.
  -c FILE --conf=FILE    Path of the ldap.conf (default is /etc/ldap.conf).
                         The ldap.conf is not required when at least --base is
                         provided.
  -D DN --binddn=DN      DN to bind with instead of the user's DN.
  -H URI... --uri=URI... URI of the LDAP server to connect; loaded from the
                         config file by default. If not defined even there,
                         then it defaults to ldap://localhost.
  -q --quiet             Be quiet.
  -u LOGIN --user=LOGIN  Login of the user to bind as and change public key(s)
                         (default is the current user).
  -v --version           Show version information.
  -h --help              Show this message.
"""

from __future__ import print_function

import sys
from docopt import docopt
from getpass import getpass, getuser
from os import access, R_OK
from ssh_ldap_pubkey import LdapSSH, Error, keyname
from ssh_ldap_pubkey.config import LdapConfig
from ssh_ldap_pubkey import __versionstr__


DEFAULT_CONFIG_PATH = '/etc/ldap.conf'

quiet = False


def read_file(path):
    """Read file and return its content with stripped newlines.

    This is foolproof against some users that are unable to copy keys properly.
    """
    with open(path, 'r') as f:
        return ''.join(f.readlines()).strip()


def read_stdin():
    """Read from standard input and strip newlines.

    This is foolproof against some users that are unable to copy keys properly.
    """
    return ''.join(sys.stdin.readlines()).strip()


def halt(msg, code=1):
    """Print error message to stderr and exit with the specified code."""
    print('Error: ' + msg, file=sys.stderr)
    exit(code)


def info(msg, *args):
    """Print message to stderr unless we're quiet."""
    if not quiet:
        print(msg % args, file=sys.stderr)


def main(**kwargs):
    global quiet

    quiet = kwargs['--quiet']
    confpath = kwargs['--conf'] or DEFAULT_CONFIG_PATH
    login = kwargs['--user'] or getuser()
    passw = None

    if not access(confpath, R_OK):
        info("Notice: Could not read config: %s; running with defaults.", confpath)
        confpath = None

    conf = LdapConfig(confpath)
    if kwargs['--uri']:
        conf.uris = kwargs['--uri']
    if kwargs['--base']:
        conf.base = kwargs['--base']

    # prompt for password
    if kwargs['--binddn']:
        conf.bind_dn = kwargs['--binddn']
        conf.bind_pass = getpass("Enter LDAP password for '%s': " % conf.bind_dn)
    elif kwargs['add'] or kwargs['del']:
        passw = getpass("Enter login (LDAP) password for user '%s': " % login)

    ldapssh = LdapSSH(conf)

    try:
        ldapssh.connect()

        if kwargs['add']:
            filesrc = kwargs['FILE'] and kwargs['FILE'] != '-'
            pubkey = read_file(kwargs['FILE']) if filesrc else read_stdin()

            ldapssh.add_pubkey(login, passw, pubkey)
            info("Key has been stored: %s", keyname(pubkey))

        elif kwargs['del']:
            keys = ldapssh.find_and_remove_pubkeys(login, passw, kwargs['PATTERN'])
            if keys:
                info('Deleted keys:')
                print('\n'.join(keys))
            else:
                info('No keys found to delete.')

        else:  # list
            keys = ldapssh.find_pubkeys(login)
            if keys:
                print('\n'.join(keys))
            else:
                info('No public keys found.')

    except Error as e:
        halt(str(e), e.code)

    except IOError as e:
        halt("%s: %s" % (e.strerror, e.filename), 1)

    finally:
        ldapssh.close()


if __name__ == '__main__':
    kwargs = docopt(__doc__, version="ssh-ldap-pubkey %s" % __versionstr__)
    main(**kwargs)


================================================
FILE: bin/ssh-ldap-pubkey-wrapper
================================================
#!/bin/sh
#
# Wrapper script for ssh-ldap-pubkey to be used as AuthorizedKeysCommand
# in OpenSSHd.
set -eu

SSH_USER="$1"

KEYS="$(ssh-ldap-pubkey list -q -u "$SSH_USER")"
KEYS_COUNT="$(printf '%s\n' "$KEYS" | grep '^ssh' | wc -l)"

logger -t sshd -p info \
	"Loaded ${KEYS_COUNT} SSH public key(s) from LDAP for user: ${SSH_USER}"

printf '%s\n' "$KEYS"


================================================
FILE: etc/ldap.conf
================================================
# /etc/ldap.conf
#
# This is the configuration file for OpenSSH LDAP Public Keys (ssh-ldap-pubkey).
#
# This file actually uses a subset of directives from configuration file of the
# LDAP nameservice switch library and the LDAP PAM module, so the same file can
# be used for all these services.
#

# Specifies the URI(s) of the LDAP server(s) to connect to. The URI scheme may
# be ldap, or ldaps, specifying LDAP over TCP or SSL respectively. A port
# number can be specified; the default port number for the selected protocol
# is used if omitted.
uri ldap://localhost

# The distinguished name of the search base.
base dc=example,dc=org

# The LDAP version to use. Default is 3 if supported by client library.
#ldap_version 3

# Enable SASL and specify mechanism to use (currently supported: GSSAPI).
#sasl GSSAPI

# The distinguished name to bind to the server with.
# Default is to bind anonymously.
#binddn cn=proxyuser,dc=example,dc=org

# The credentials to bind with. Default is no credential.
#bindpw secret

# The search scope; sub, one, or base.
scope one

# Specifies if the client should automatically follow referrals returned
# by LDAP servers. This must be typically disabled for Active Directory.
# Default is "on".
referrals off

# Search timelimit in seconds (0 for indefinite).
timelimit 5

# Bind/connect timelimit (0 for indefinite).
bind_timelimit 5

# The filter to use when retrieving user information, additional to the login
# attribute value assertion (pam_login_attribute=<login>).
#pam_filter objectclass=posixAccount

# The user ID attribute (defaults to 'uid').
#pam_login_attribute uid

# RFC2307bis naming contexts
# Syntax is:
#   nss_base_XXX  base?scope?filter
# where scope is {base,one,sub} and filter is a filter to be &'d with the
# default filter.
nss_base_passwd	      ou=People,dc=example,dc=org?one

# CA certificates for server certificate verification.
tls_cacertdir /etc/ssl/certs

# Name of LDAP attribute used for SSH public keys.
pubkey_attr sshPublicKey


================================================
FILE: etc/openssh-lpk.schema
================================================
#
# LDAP Public Key Patch schema for use with openssh-ldappubkey
# Author: Eric AUGE <eau@phear.org>
#
# Based on the proposal of : Mark Ruijter
#


# octetString SYNTAX
attributetype ( 1.3.6.1.4.1.24552.500.1.1.1.13 NAME 'sshPublicKey'
	DESC 'OpenSSH Public key'
	EQUALITY octetStringMatch
	SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )

# printableString SYNTAX yes|no
objectclass ( 1.3.6.1.4.1.24552.500.1.1.2.0 NAME 'ldapPublicKey' SUP top AUXILIARY
	DESC 'OpenSSH LPK objectclass'
	MUST uid
	MAY sshPublicKey
	)


================================================
FILE: requirements.txt
================================================
pycodestyle
pytest
pytest-cov
pytest-describe
pytest-mock


================================================
FILE: setup.cfg
================================================
[pycodestyle]
max-line-length = 100
# E241 and E302 should be excluded only in tests, but I don't know how to configure it.
ignore = E701,E731,E241,E302
exclude = .git/,build/,dist/,docs/,setup.py

[tools:pytest]
describe_prefixes = describe_ context_


================================================
FILE: setup.py
================================================
#!/usr/bin/env python3
import sys
from setuptools import setup

setup(
    name='ssh-ldap-pubkey',
    version='1.4.0',
    url='https://github.com/jirutka/ssh-ldap-pubkey',
    description='Utility to manage SSH public keys stored in LDAP.',
    long_description=open('README.md', 'r').read(),
    long_description_content_type='text/markdown',
    author='Jakub Jirutka',
    author_email='jakub@jirutka.cz',
    license='MIT',
    packages=['ssh_ldap_pubkey'],
    scripts=['bin/ssh-ldap-pubkey', 'bin/ssh-ldap-pubkey-wrapper'],
    install_requires=[
        'docopt>=0.6.2,<0.7.0',
        'python-ldap>=3.0.0,<4'
    ],
    classifiers=[
        'Environment :: Console',
        'Intended Audience :: Developers',
        'License :: OSI Approved :: MIT License',
        'Operating System :: POSIX',
        'Programming Language :: Python :: 3',
        'Topic :: System',
        'Topic :: Utilities'
    ]
)


================================================
FILE: ssh_ldap_pubkey/__init__.py
================================================
# -*- coding: utf-8 -*-
import base64
import ldap
import struct
import sys

from .exceptions import *


VERSION = (1, 4, 0)
__version__ = VERSION
__versionstr__ = '.'.join(map(str, VERSION))

BAD_REQCERT_WARNING = u'''
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! WARNING: You've choosen to ignore TLS errors such as invalid certificate. !!
!! This is a VERY BAD thing, never ever use this in production! ᕦ(ò_óˇ)ᕤ     !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
'''


def keyname(pubkey):
    return pubkey.split()[-1]


def is_valid_openssh_pubkey(pubkey):
    """Check if the given string is a valid OpenSSH public key.

    This function is based on http://stackoverflow.com/a/2494645/2217862.

    Arguments:
        pubkey (str): The string to validate.
    Returns:
        bool: `True` if the given string is a valid key, `False` otherwise.
    """
    try:
        key_type, data64 = map(_encode, pubkey.split()[0:2])
    except (ValueError, AttributeError):
        return False
    try:
        data = base64.decodebytes(data64)
    except base64.binascii.Error:
        return False

    int_len = 4
    str_len = struct.unpack('>I', data[:int_len])[0]

    if data[int_len:(int_len + str_len)] != key_type:
        return False

    return True


def _decode(input):
    return input.decode('utf8')


def _encode(input):
    return input.encode('utf8')


class LdapSSH(object):

    def __init__(self, conf):
        """Initialize new LdapSSH instance.

        Arguments:
            conf (LdapConfig): The LDAP configuration.
        """
        self.conf = conf
        self._conn = None

    def connect(self):
        """Connect to the LDAP server.
        This method must be called before any other methods of this object.

        Raises:
            ConfigError: If Base DN or LDAP URI is missing in the config.
            LDAPConnectionError: If can't connect to the LDAP server.
            ldap.LDAPError:
        """
        conf = self.conf

        if not conf.uris or not conf.base:
            raise ConfigError('Base DN and LDAP URI(s) must be provided.', 1)

        if conf.tls_require_cert is not None:
            if conf.tls_require_cert not in [ldap.OPT_X_TLS_DEMAND, ldap.OPT_X_TLS_HARD]:
                print(BAD_REQCERT_WARNING, file=sys.stderr)
            # this is a global option!
            ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, conf.tls_require_cert)

        if conf.cacert_dir:
            # this is a global option!
            ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, conf.cacert_dir)

        if not conf.referrals:
            # this is a global option!
            ldap.set_option(ldap.OPT_REFERRALS, 0)

        # NOTE: The uri argument is passed directly to the underlying openldap
        # library that allows multiple URIs separated by a space for failover.
        self._conn = conn = ldap.initialize(' '.join(conf.uris))
        try:
            conn.protocol_version = conf.ldap_version
            conn.network_timeout = conf.bind_timeout
            conn.timeout = conf.search_timeout

            if conf.sasl == 'GSSAPI':
                self._bind_sasl_gssapi()
                return

            if conf.ssl == 'start_tls' and conf.ldap_version >= 3:
                conn.start_tls_s()

            if conf.bind_dn and conf.bind_pass:
                self._bind(conf.bind_dn, conf.bind_pass)
        except ldap.SERVER_DOWN:
            raise LDAPConnectionError('Can\'t contact LDAP server.', 3)

    def close(self):
        """Unbind from the LDAP server."""
        self._conn and self._conn.unbind_s()

    def add_pubkey(self, login, password, pubkey):
        """Add SSH public key to the user with the given ``login``.

        Arguments:
            login (str): Login of the user to add the ``pubkey``.
            password (Optional[str]): The user's password to bind with, or None
                to not (re)bind with the user's credentials.
            pubkey (str): The public key to add.
        Raises:
            InvalidPubKeyError: If the ``pubkey`` is invalid.
            PubKeyAlreadyExistsError: If the user already has the given ``pubkey``.
            UserEntryNotFoundError: If the ``login`` is not found.
            ConfigError: If LDAP server doesn't define schema for the attribute specified
                in the config.
            InsufficientAccessError: If the bind user doesn't have rights to add the pubkey.
            ldap.LDAPError:
        """
        conf = self.conf

        if not is_valid_openssh_pubkey(pubkey):
            raise InvalidPubKeyError('Invalid key, not in OpenSSH Public Key format.', 1)

        dn = self.find_dn_by_login(login)
        if password:
            self._bind(dn, password)

        if self._has_pubkey(dn, pubkey):
            raise PubKeyAlreadyExistsError(
                "Public key %s already exists." % keyname(pubkey), 1)

        modlist = [(ldap.MOD_ADD, conf.pubkey_attr, _encode(pubkey))]
        try:
            self._conn.modify_s(dn, modlist)

        except ldap.OBJECT_CLASS_VIOLATION:
            modlist += [(ldap.MOD_ADD, 'objectClass', _encode(conf.pubkey_class))]
            self._conn.modify_s(dn, modlist)

        except ldap.UNDEFINED_TYPE:
            raise ConfigError(
                "LDAP server doesn't define schema for attribute: %s" % conf.pubkey_attr, 1)

        except ldap.INSUFFICIENT_ACCESS:
            raise InsufficientAccessError("No rights to add key for %s " % dn, 2)

    def find_and_remove_pubkeys(self, login, password, pattern):
        """Find and remove public keys of the user with the ``login`` that maches the ``pattern``.

        Arguments:
            login (str): Login of the user to add the ``pubkey``.
            password (Optional[str]): The user's password to bind with, or None
                to not (re)bind with the user's credentials.
            pattern (str): The pattern specifying public keys to be removed.
        Raises:
            UserEntryNotFoundError: If the ``login`` is not found.
            NoPubKeyFoundError: If no public key matching the ``pattern`` is found.
            InsufficientAccessError: If the bind user doesn't have rights to add the pubkey.
            ldap.LDAPError:
        Returns:
            List[str]: A list of removed public keys.
        """
        dn = self.find_dn_by_login(login)
        if password:
            self._bind(dn, password)

        pubkeys = [key for key in self._find_pubkeys(dn) if pattern in key]
        for key in pubkeys:
            self._remove_pubkey(dn, key)

        return pubkeys

    def find_pubkeys(self, login):
        """Return public keys of the user with the given ``login``.

        Arguments:
            login (str): The login name of the user.
        Returns:
            List[str]: A list of public keys.
        Raises:
            UserEntryNotFoundError: If the ``login`` is not found.
            ldap.LDAPError:
        """
        return self._find_pubkeys(self.find_dn_by_login(login))

    def find_dn_by_login(self, login):
        """Returns Distinguished Name (DN) of the user with the given ``login``.

        Arguments:
            login (str): The login name of the user to find.
        Returns:
            str: User's DN.
        Raises:
            UserEntryNotFoundError: If the ``login`` is not found.
            ldap.LDAPError:
        """
        conf = self.conf
        filter_s = conf.filter
        # RFC4515 requires filters to be wrapped with parenthesis '(' and ')'.
        # Over-wrapped filters are invalid! e.g. '((uid=x))'
        #
        # OpenLDAP permits simple filters to omit parenthesis entirely:
        # e.g. 'uid=x' is automatically treated as '(uid=x)'
        #
        # The OpenLDAP behavior is taken as a given in many uses, which can
        # lead to bad assumptions merging filters, because over-wrapped filters
        # ARE still rejected.
        #
        # To cope with these cases, only wrap the incoming filter in
        # parenthesis if it does NOT already have them.
        if filter_s[0] != '(':
            filter_s = '(%s)' % filter_s
        filter_s = "(&%s(%s=%s))" % (filter_s, conf.login_attr, login)

        result = self._conn.search_s(conf.base, conf.scope, filter_s, ['dn'])
        if not result:
            raise UserEntryNotFoundError("No user with login '%s' found." % login, 2)

        return result[0][0]

    def _bind(self, dn, password):
        try:
            self._conn.simple_bind_s(dn, password)
        except ldap.INVALID_CREDENTIALS:
            raise InvalidCredentialsError("Invalid credentials for %s." % dn, 2)

    def _bind_sasl_gssapi(self):
        self._conn.sasl_interactive_bind_s('', ldap.sasl.sasl({}, 'GSSAPI'))

    def _find_pubkeys(self, dn):
        conf = self.conf
        result = self._conn.search_s(
            dn, ldap.SCOPE_BASE, attrlist=[conf.pubkey_attr])

        return map(_decode, result[0][1].get(conf.pubkey_attr, []))

    def _has_pubkey(self, dn, pubkey):
        current = self._find_pubkeys(dn)
        is_same_key = lambda k1, k2: k1.split()[1] == k2.split()[1]

        return any(key for key in current if is_same_key(key, pubkey))

    def _remove_pubkey(self, dn, pubkey):
        conf = self.conf

        modlist = [(ldap.MOD_DELETE, conf.pubkey_attr, _encode(pubkey))]
        try:
            self._conn.modify_s(dn, modlist)

        except ldap.OBJECT_CLASS_VIOLATION:
            modlist += [(ldap.MOD_DELETE, 'objectClass', _encode(conf.pubkey_class))]
            self._conn.modify_s(dn, modlist)

        except ldap.NO_SUCH_ATTRIBUTE:
            raise NoPubKeyFoundError("No such public key exists: %s." % keyname(pubkey), 1)

        except ldap.INSUFFICIENT_ACCESS:
            raise InsufficientAccessError("No rights to remove key for %s " % dn, 2)


================================================
FILE: ssh_ldap_pubkey/config.py
================================================
# -*- coding: utf-8 -*-
import ldap
import re

DEFAULT_FILTER = 'objectclass=posixAccount'
DEFAULT_HOST = 'localhost'
DEFAULT_LOGIN_ATTR = 'uid'
DEFAULT_PORT = 389
DEFAULT_PUBKEY_ATTR = 'sshPublicKey'
DEFAULT_PUBKEY_CLASS = 'ldapPublicKey'
DEFAULT_REFERRALS = 'on'
DEFAULT_SCOPE = 'sub'
DEFAULT_TIMEOUT = 10


def parse_config(content):
    """Parse configuration options into a dict.

    Blank lines are ignored. Lines beginning with a hash mark (`#`) are comments, and ignored.
    Valid lines are made of an option's name (a sequence of non-blanks), followed by a value.
    The value starts with the first non-blank character after the option's name, and terminates at
    the end of the line, or at the last sequence of blanks before the end of the line.
    Option names are case-insensitive, and converted to lower-case.

    Arguments:
        content (str): The content of a configuration file to parse.
    Returns:
        dict: Parsed options.
    """
    return {
        match.group(1).lower(): match.group(2).strip()
        for match in (
            re.match(r'^(\w+)\s+([^#]+)', line)
            for line in content.splitlines()
        ) if match
    }


def parse_config_file(path):
    """Same as :func:`parse_config`, but read options from a file.

    Arguments:
        path (str): Path of the file to read and parse.
    Returns:
        dict: Parsed options.
    """
    with open(path, 'r') as f:
        return parse_config(f.read())


def parse_bool(value):
    """Parse string that represents a boolean value.

    Arguments:
        value (str): The value to parse.
    Returns:
        bool: True if the value is "on", "true", or "yes", False otherwise.
    """
    return (value or '').lower() in ('on', 'true', 'yes')


def parse_tls_reqcert_opt(value):
    """Convert `tls_reqcert` option to ldap's `OPT_X_TLS_*` constant."""
    return {
        'never': ldap.OPT_X_TLS_NEVER,
        'allow': ldap.OPT_X_TLS_ALLOW,
        'try': ldap.OPT_X_TLS_TRY,
        'demand': ldap.OPT_X_TLS_DEMAND,
        'hard': ldap.OPT_X_TLS_HARD
    }[value.lower()] if value else None


def parse_scope_opt(value):
    """Convert `scope` option to ldap's `SCOPE_*` constant."""
    return {
        'base': ldap.SCOPE_BASE,
        'one': ldap.SCOPE_ONELEVEL,
        'sub': ldap.SCOPE_SUBTREE
    }[value.lower()] if value else None


class LdapConfig(object):

    def __init__(self, path):
        """Initialize new LdapConfig with options parsed from config file on the ``path``.

        Arguments:
            path (Optional[path]): Path to the config file to read and parse.
                If not provided, then empty config is initialized.
        """
        conf = parse_config_file(path) if path else {}

        if 'uri' in conf:
            self.uris = conf['uri'].split()
        else:
            host = conf.get('host', DEFAULT_HOST)
            port = conf.get('port', DEFAULT_PORT)
            self.uris = ["ldap://%s:%s" % (host, port)]

        self.base = conf.get('nss_base_passwd', '').split('?')[0] or conf.get('base', None)
        self.bind_dn = conf.get('binddn', None)
        self.bind_pass = conf.get('bindpw', None)
        self.bind_timeout = int(conf.get('bind_timelimit', DEFAULT_TIMEOUT))
        self.cacert_dir = conf.get('tls_cacertdir', None)
        self.filter = conf.get('pam_filter', DEFAULT_FILTER)
        self.ldap_version = int(conf.get('ldap_version', ldap.VERSION3))
        self.login_attr = conf.get('pam_login_attribute', DEFAULT_LOGIN_ATTR)
        self.pubkey_attr = conf.get('pubkey_attr', DEFAULT_PUBKEY_ATTR)
        self.pubkey_class = conf.get('pubkey_class', DEFAULT_PUBKEY_CLASS)
        self.referrals = parse_bool(conf.get('referrals', DEFAULT_REFERRALS))
        self.sasl = conf.get('sasl', None)
        self.scope = parse_scope_opt(conf.get('scope', DEFAULT_SCOPE))
        self.search_timeout = int(conf.get('timelimit', DEFAULT_TIMEOUT))
        self.ssl = conf.get('ssl', None)
        self.tls_require_cert = parse_tls_reqcert_opt(conf.get('tls_reqcert'))

    @property
    def uri(self):  # for backward compatibility with <1.1.0
        return self.uris[0] if self.uris else None

    @uri.setter
    def uri(self, uri):  # for backward compatibility with <1.1.0
        self.uris = [uri] if uri else None

    def __str__(self):
        return str(self.__dict__)


================================================
FILE: ssh_ldap_pubkey/exceptions.py
================================================
# -*- coding: utf-8 -*-

class Error(Exception):

    def __init__(self, msg, code=1):
        self.msg = msg
        self.code = code

    def __str__(self):
        return self.msg


class ConfigError(Error): pass
class InsufficientAccessError(Error): pass
class InvalidCredentialsError(Error): pass
class InvalidPubKeyError(Error): pass
class LDAPConnectionError(Error): pass
class NoPubKeyFoundError(Error): pass
class PubKeyAlreadyExistsError(Error): pass
class UserEntryNotFoundError(Error): pass


================================================
FILE: tests/__init__.py
================================================


================================================
FILE: tests/config_test.py
================================================
# -*- coding: utf-8 -*-
from inspect import cleandoc
from io import StringIO
from pytest import mark, raises
from ssh_ldap_pubkey.config import *
from textwrap import dedent


def describe_parse_config():

    def parses_key_value_separated_by_whitespace():
        content = dedent('''\
            uri  ldap://localhost
            base dc=example, dc=org
            ldap_version\t\t3
        ''')
        expected = {
            'uri': 'ldap://localhost',
            'base': 'dc=example, dc=org',
            'ldap_version': '3'
        }
        assert parse_config(content) == expected

    def strips_trailing_whitespaces():
        content = dedent('''\
            scope base\t
            timelimit 3\t\t
        ''')
        expected = {
            'scope': 'base',
            'timelimit': '3'
        }
        assert parse_config(content) == expected

    def ignores_comments():
        content = dedent('''\
            # The search scope; sub, one, or base.
            scope one
            #timelimit 5
        ''')
        expected = {
            'scope': 'one'
        }
        assert parse_config(content) == expected

    def converts_keys_to_lowercase():
        content = dedent('''\
            ScoPe base
            BASE DC=Example,DC=org
        ''')
        expected = {
            'scope': 'base',
            'base': 'DC=Example,DC=org'
        }
        assert parse_config(content) == expected


def describe_parse_config_file():

    def reads_file_and_calls_parse_config(mocker):
        # This removes the leading whitespace in this file for the test.
        content = cleandoc(u'''
        scope one
        timelimit 3
        pam_filter (objectclass=posixAccount)
        nss_reconnect_tries 2 # number of times to double the sleep time
        trailing_space_after_variable foobar
        ''')  # noqa
        open_mock = mocker.patch('builtins.open', return_value=StringIO(initial_value=content))

        result = {
            'scope': 'one',
            'timelimit': '3',
            'pam_filter': '(objectclass=posixAccount)',
            'nss_reconnect_tries': '2',
            'trailing_space_after_variable': 'foobar',
        }
        parse_config_mock = mocker.patch('ssh_ldap_pubkey.config.parse_config', return_value=result)

        assert parse_config_file('/etc/ldap.conf') == result
        open_mock.assert_called_with('/etc/ldap.conf', 'r')
        parse_config_mock.assert_called_with(content)


def describe_parse_bool():

    @mark.parametrize('value', ['on', 'true', 'yes', 'ON', 'TrUe'])
    def returns_True_when_given_truthy_value(value):
        assert parse_bool(value)

    @mark.parametrize('value', ['off', 'false', 'no', 'NO', 'fooo'])
    def returns_False_when_given_non_truthy_value(value):
        assert not parse_bool(value)

    def returns_False_when_given_None():
        assert not parse_bool(None)


def describe_parse_tls_reqcert_opt():

    @mark.parametrize('value, expected', [
        ('never',  ldap.OPT_X_TLS_NEVER),
        ('allow',  ldap.OPT_X_TLS_ALLOW),
        ('try',    ldap.OPT_X_TLS_TRY),
        ('demand', ldap.OPT_X_TLS_DEMAND),
        ('hard',   ldap.OPT_X_TLS_HARD),
    ])
    def returns_ldap_OPT_X_TLS_constant_for_valid_value(value, expected):
        assert parse_tls_reqcert_opt(value) == expected
        assert parse_tls_reqcert_opt(value.upper()) == expected

    @mark.parametrize('value', [None, ''])
    def returns_None_when_given_falsy(value):
        assert parse_tls_reqcert_opt(value) is None

    def raises_KeyError_for_invalud_value():
        with raises(KeyError):
            parse_tls_reqcert_opt('whatever')


def describe_parse_scope_opt():

    @mark.parametrize('value, expected', [
        ('base', ldap.SCOPE_BASE),
        ('one',  ldap.SCOPE_ONELEVEL),
        ('sub',  ldap.SCOPE_SUBTREE)
    ])
    def returns_ldap_SCOPE_constant_for_valid_value(value, expected):
        assert parse_scope_opt(value) == expected
        assert parse_scope_opt(value.upper()) == expected

    @mark.parametrize('value', [None, ''])
    def returns_None_when_given_falsy(value):
        assert parse_scope_opt(value) is None

    def raises_KeyError_for_invalud_value():
        with raises(KeyError):
            parse_scope_opt('whatever')


================================================
FILE: tests/fixtures/invalid_ssh_keys
================================================
ssh-rsa

AAAAB3NzaC1yc2EAAAADAQABAAAAYQCkNCKyyUPvwwefhEpnD/Khuowbv+13Cv3shWY3ck3j+hbHLqjCGUB66/igg0Atf3giU7sZOdPrSN6xdX43+k6Gn73yzWFJkWyImKzgHW9uVrU7vo3puailY+yC6RpwNME= flynn@encom.com
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAYQCkNCKyyUPvwwefhEpnD/Khuowbv+13Cv3shWY3ck3j+hbHLqjCGUB66/igg0Atf3giU7sZOdPrSN6xdX43+k6Gn73yzWFJkWyImKzgHW9uVrU7vo3puailY+yC6RpwNM
ssh-dss AAAAB3NzaC1yc2EAAAADAQABAAAAYQCkNCKyyUPvwwefhEpnD/Khuowbv+13Cv3shWY3ck3j+hbHLqjCGUB66/igg0Atf3giU7sZOdPrSN6xdX43+k6Gn73yzWFJkWyImKzgHW9uVrU7vo3puailY+yC6RpwNME=


================================================
FILE: tests/fixtures/valid_ssh_keys
================================================
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAYQCkNCKyyUPvwwefhEpnD/Khuowbv+13Cv3shWY3ck3j+hbHLqjCGUB66/igg0Atf3giU7sZOdPrSN6xdX43+k6Gn73yzWFJkWyImKzgHW9uVrU7vo3puailY+yC6RpwNME= flynn@encom.com
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAYQCkNCKyyUPvwwefhEpnD/Khuowbv+13Cv3shWY3ck3j+hbHLqjCGUB66/igg0Atf3giU7sZOdPrSN6xdX43+k6Gn73yzWFJkWyImKzgHW9uVrU7vo3puailY+yC6RpwNME=
ssh-dss AAAAB3NzaC1kc3MAAACBAKMfNb7OtqUc5vje5UMvm5r1javDreL+EvACyU7N/BqO+FnqYx6RWQgp0mgDW5b+eOVBBwCfqn41TKHoXIlOsD3Rci7REBA87jmRbgQnZLSaT6BgVFWtEMI8SXG3EsmC5fXKHfX8j6W6f0Tuqeof4eaFW2RC6pYN3rvVGCvJ68FXAAAAFQDbmXw8U17CkJDvFOZ8KqehrahunQAAAIAKg/NxJVqLTJOM6qp7zI0WOiStotzclC5899w3zqJ446P/jLD5nILpnhbnUHnGmw53Yz6endWKHInsUurODPRZcjgOOd2lG5/RViegrI81YsQtczzIRNU86xL/28UkzzAy1IMeJWiH2vSrLLzx37YKZFH4MBjPhlsFtEIfHeR3dQAAAIEAlAMlPaHMIMEP+4IQP3qUhcZhI8UIYlSag2DqRjPry9jRL6uaYUYZAPC82Bvfi83TqJEhowqIBDFib4HDrgp9BJyTGfDZsHjRcLP15FYQdqIg8Fdo2ptsWGJa6EdFmj8RDIyaHsQ4a/JtnHUu+nC7DMYvTHliWl55UUVCSJzAyzs=


================================================
FILE: tests/functions_test.py
================================================
# -*- coding: utf-8 -*-
from pytest import mark
from os import path
from ssh_ldap_pubkey import is_valid_openssh_pubkey

FIXTURES_DIR = path.dirname(__file__) + '/fixtures'


def describe_is_valid_openssh_pubkey():

    @mark.parametrize('key', read_fixtures('valid_ssh_keys'))
    def returns_true_when_given_valid_pubkey(key):
        assert is_valid_openssh_pubkey(key)

    @mark.parametrize('key', read_fixtures('invalid_ssh_keys'))
    def returns_false_when_given_invalid_pubkey(key):
        assert not is_valid_openssh_pubkey(key)


def read_fixtures(filename):
    with open(path.join(FIXTURES_DIR, filename)) as f:
        return f.readlines()
Download .txt
gitextract_5m6ot60j/

├── .editorconfig
├── .github/
│   ├── FUNDING.yml
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── CHANGELOG.adoc
├── LICENSE
├── MANIFEST.in
├── README.md
├── bin/
│   ├── ssh-ldap-pubkey
│   └── ssh-ldap-pubkey-wrapper
├── etc/
│   ├── ldap.conf
│   └── openssh-lpk.schema
├── requirements.txt
├── setup.cfg
├── setup.py
├── ssh_ldap_pubkey/
│   ├── __init__.py
│   ├── config.py
│   └── exceptions.py
└── tests/
    ├── __init__.py
    ├── config_test.py
    ├── fixtures/
    │   ├── invalid_ssh_keys
    │   └── valid_ssh_keys
    └── functions_test.py
Download .txt
SYMBOL INDEX (45 symbols across 5 files)

FILE: ssh_ldap_pubkey/__init__.py
  function keyname (line 22) | def keyname(pubkey):
  function is_valid_openssh_pubkey (line 26) | def is_valid_openssh_pubkey(pubkey):
  function _decode (line 54) | def _decode(input):
  function _encode (line 58) | def _encode(input):
  class LdapSSH (line 62) | class LdapSSH(object):
    method __init__ (line 64) | def __init__(self, conf):
    method connect (line 73) | def connect(self):
    method close (line 121) | def close(self):
    method add_pubkey (line 125) | def add_pubkey(self, login, password, pubkey):
    method find_and_remove_pubkeys (line 170) | def find_and_remove_pubkeys(self, login, password, pattern):
    method find_pubkeys (line 196) | def find_pubkeys(self, login):
    method find_dn_by_login (line 209) | def find_dn_by_login(self, login):
    method _bind (line 244) | def _bind(self, dn, password):
    method _bind_sasl_gssapi (line 250) | def _bind_sasl_gssapi(self):
    method _find_pubkeys (line 253) | def _find_pubkeys(self, dn):
    method _has_pubkey (line 260) | def _has_pubkey(self, dn, pubkey):
    method _remove_pubkey (line 266) | def _remove_pubkey(self, dn, pubkey):

FILE: ssh_ldap_pubkey/config.py
  function parse_config (line 16) | def parse_config(content):
  function parse_config_file (line 39) | def parse_config_file(path):
  function parse_bool (line 51) | def parse_bool(value):
  function parse_tls_reqcert_opt (line 62) | def parse_tls_reqcert_opt(value):
  function parse_scope_opt (line 73) | def parse_scope_opt(value):
  class LdapConfig (line 82) | class LdapConfig(object):
    method __init__ (line 84) | def __init__(self, path):
    method uri (line 118) | def uri(self):  # for backward compatibility with <1.1.0
    method uri (line 122) | def uri(self, uri):  # for backward compatibility with <1.1.0
    method __str__ (line 125) | def __str__(self):

FILE: ssh_ldap_pubkey/exceptions.py
  class Error (line 3) | class Error(Exception):
    method __init__ (line 5) | def __init__(self, msg, code=1):
    method __str__ (line 9) | def __str__(self):
  class ConfigError (line 13) | class ConfigError(Error): pass
  class InsufficientAccessError (line 14) | class InsufficientAccessError(Error): pass
  class InvalidCredentialsError (line 15) | class InvalidCredentialsError(Error): pass
  class InvalidPubKeyError (line 16) | class InvalidPubKeyError(Error): pass
  class LDAPConnectionError (line 17) | class LDAPConnectionError(Error): pass
  class NoPubKeyFoundError (line 18) | class NoPubKeyFoundError(Error): pass
  class PubKeyAlreadyExistsError (line 19) | class PubKeyAlreadyExistsError(Error): pass
  class UserEntryNotFoundError (line 20) | class UserEntryNotFoundError(Error): pass

FILE: tests/config_test.py
  function describe_parse_config (line 9) | def describe_parse_config():
  function describe_parse_config_file (line 58) | def describe_parse_config_file():
  function describe_parse_bool (line 85) | def describe_parse_bool():
  function describe_parse_tls_reqcert_opt (line 99) | def describe_parse_tls_reqcert_opt():
  function describe_parse_scope_opt (line 121) | def describe_parse_scope_opt():

FILE: tests/functions_test.py
  function describe_is_valid_openssh_pubkey (line 9) | def describe_is_valid_openssh_pubkey():
  function read_fixtures (line 20) | def read_fixtures(filename):
Condensed preview — 23 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (46K chars).
[
  {
    "path": ".editorconfig",
    "chars": 280,
    "preview": "# http://editorconfig.org/\nroot = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\nindent_style = space\nindent_size = 4\ninsert"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 63,
    "preview": "# These are supported funding model platforms\n\ngithub: jirutka\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 1448,
    "preview": "name: CI\non:\n  - push\n  - pull_request\n\njobs:\n  test:\n    name: Test on Python ${{ matrix.python }}\n    runs-on: ubuntu-"
  },
  {
    "path": ".gitignore",
    "chars": 178,
    "preview": "# Byte-compiled / optimized files\n__pycache__/\n*.py[cod]\n\n# Distribution / packaging\nenv/\nbuild/\ndist/\n/MANIFEST\n*.egg-i"
  },
  {
    "path": "CHANGELOG.adoc",
    "chars": 3224,
    "preview": "= Changelog\n:repo-uri: https://github.com/jirutka/ssh-ldap-pubkey\n:issues: {repo-uri}/issues\n:pulls: {repo-uri}/pull\n:ta"
  },
  {
    "path": "LICENSE",
    "chars": 1098,
    "preview": "The MIT License\n\nCopyright 2014-present Jakub Jirutka <jakub@jirutka.cz>.\n\nPermission is hereby granted, free of charge,"
  },
  {
    "path": "MANIFEST.in",
    "chars": 130,
    "preview": "include README.md\ninclude CHANGELOG.adoc\ninclude LICENSE\ninclude MANIFEST.in\ninclude etc/ldap.conf\ninclude etc/openssh-l"
  },
  {
    "path": "README.md",
    "chars": 7211,
    "preview": "OpenSSH / LDAP public keys\n==========================\n[![Build Status](https://github.com/jirutka/ssh-ldap-pubkey/workfl"
  },
  {
    "path": "bin/ssh-ldap-pubkey",
    "chars": 4447,
    "preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n\n\"\"\"\nssh-ldap-pubkey - Utility to manage SSH public keys stored in LDAP.\n\n"
  },
  {
    "path": "bin/ssh-ldap-pubkey-wrapper",
    "chars": 356,
    "preview": "#!/bin/sh\n#\n# Wrapper script for ssh-ldap-pubkey to be used as AuthorizedKeysCommand\n# in OpenSSHd.\nset -eu\n\nSSH_USER=\"$"
  },
  {
    "path": "etc/ldap.conf",
    "chars": 2008,
    "preview": "# /etc/ldap.conf\n#\n# This is the configuration file for OpenSSH LDAP Public Keys (ssh-ldap-pubkey).\n#\n# This file actual"
  },
  {
    "path": "etc/openssh-lpk.schema",
    "chars": 510,
    "preview": "#\n# LDAP Public Key Patch schema for use with openssh-ldappubkey\n# Author: Eric AUGE <eau@phear.org>\n#\n# Based on the pr"
  },
  {
    "path": "requirements.txt",
    "chars": 58,
    "preview": "pycodestyle\npytest\npytest-cov\npytest-describe\npytest-mock\n"
  },
  {
    "path": "setup.cfg",
    "chars": 252,
    "preview": "[pycodestyle]\nmax-line-length = 100\n# E241 and E302 should be excluded only in tests, but I don't know how to configure "
  },
  {
    "path": "setup.py",
    "chars": 919,
    "preview": "#!/usr/bin/env python3\nimport sys\nfrom setuptools import setup\n\nsetup(\n    name='ssh-ldap-pubkey',\n    version='1.4.0',\n"
  },
  {
    "path": "ssh_ldap_pubkey/__init__.py",
    "chars": 9903,
    "preview": "# -*- coding: utf-8 -*-\nimport base64\nimport ldap\nimport struct\nimport sys\n\nfrom .exceptions import *\n\n\nVERSION = (1, 4,"
  },
  {
    "path": "ssh_ldap_pubkey/config.py",
    "chars": 4360,
    "preview": "# -*- coding: utf-8 -*-\nimport ldap\nimport re\n\nDEFAULT_FILTER = 'objectclass=posixAccount'\nDEFAULT_HOST = 'localhost'\nDE"
  },
  {
    "path": "ssh_ldap_pubkey/exceptions.py",
    "chars": 503,
    "preview": "# -*- coding: utf-8 -*-\n\nclass Error(Exception):\n\n    def __init__(self, msg, code=1):\n        self.msg = msg\n        se"
  },
  {
    "path": "tests/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/config_test.py",
    "chars": 4274,
    "preview": "# -*- coding: utf-8 -*-\nfrom inspect import cleandoc\nfrom io import StringIO\nfrom pytest import mark, raises\nfrom ssh_ld"
  },
  {
    "path": "tests/fixtures/invalid_ssh_keys",
    "chars": 522,
    "preview": "ssh-rsa\n\nAAAAB3NzaC1yc2EAAAADAQABAAAAYQCkNCKyyUPvwwefhEpnD/Khuowbv+13Cv3shWY3ck3j+hbHLqjCGUB66/igg0Atf3giU7sZOdPrSN6xdX4"
  },
  {
    "path": "tests/fixtures/valid_ssh_keys",
    "chars": 943,
    "preview": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAYQCkNCKyyUPvwwefhEpnD/Khuowbv+13Cv3shWY3ck3j+hbHLqjCGUB66/igg0Atf3giU7sZOdPrSN6xdX43"
  },
  {
    "path": "tests/functions_test.py",
    "chars": 655,
    "preview": "# -*- coding: utf-8 -*-\nfrom pytest import mark\nfrom os import path\nfrom ssh_ldap_pubkey import is_valid_openssh_pubkey\n"
  }
]

About this extraction

This page contains the full source code of the jirutka/ssh-ldap-pubkey GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 23 files (42.3 KB), approximately 12.2k tokens, and a symbol index with 45 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!