├── .coveragerc ├── .github ├── dependabot.yml └── workflows │ ├── auto-review.yml │ ├── automerge.yml │ ├── ci-done.yml │ ├── ci.yml │ ├── codeql-analysis.yml │ ├── lint.yml │ └── release.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── COPYING ├── Dockerfile ├── MANIFEST.in ├── Makefile ├── README.md ├── THANKS ├── debian ├── README.Debian ├── changelog ├── compat ├── control ├── copyright ├── dirs ├── examples ├── gbp.conf ├── links ├── nsscache.cron ├── nsscache.manpages ├── pybuild.testfiles ├── rules ├── source │ ├── format │ └── options ├── tests │ ├── control │ └── slapd-regtest └── watch ├── doc └── mox_to_mock.md ├── examples ├── authorized-keys-command.py └── authorized-keys-command.sh ├── nss_cache ├── .DS_Store ├── __init__.py ├── app.py ├── app_test.py ├── caches │ ├── __init__.py │ ├── cache_factory.py │ ├── cache_factory_test.py │ ├── caches.py │ ├── caches_test.py │ ├── files.py │ └── files_test.py ├── command.py ├── command_test.py ├── config.py ├── config_test.py ├── error.py ├── error_test.py ├── lock.py ├── lock_test.py ├── maps │ ├── __init__.py │ ├── automount.py │ ├── automount_test.py │ ├── group.py │ ├── group_test.py │ ├── maps.py │ ├── maps_test.py │ ├── netgroup.py │ ├── netgroup_test.py │ ├── passwd.py │ ├── passwd_test.py │ ├── shadow.py │ ├── shadow_test.py │ └── sshkey.py ├── nss.py ├── nss_test.py ├── sources │ ├── __init__.py │ ├── consulsource.py │ ├── consulsource_test.py │ ├── gcssource.py │ ├── gcssource_test.py │ ├── httpsource.py │ ├── httpsource_test.py │ ├── ldapsource.py │ ├── ldapsource_test.py │ ├── s3source.py │ ├── s3source_test.py │ ├── source.py │ ├── source_factory.py │ ├── source_factory_test.py │ └── source_test.py ├── update │ ├── __init__.py │ ├── files_updater.py │ ├── files_updater_test.py │ ├── map_updater.py │ ├── map_updater_test.py │ ├── updater.py │ └── updater_test.py └── util │ ├── __init__.py │ ├── curl.py │ ├── file_formats.py │ ├── file_formats_test.py │ ├── timestamps.py │ └── timestamps_test.py ├── nsscache ├── nsscache.1 ├── nsscache.conf ├── nsscache.conf.5 ├── nsscache.cron ├── nsscache.sh ├── nsscache.spec ├── release.sh ├── requirements.txt ├── rpm ├── postinst.sh └── preinst.sh ├── setup.cfg ├── setup.py └── tests ├── default.ldif ├── nsscache.conf ├── samba.sh ├── slapd-nsscache.conf.tmpl ├── slapd-regtest └── slapd.conf.tmpl /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | include = nss_cache 3 | branch = True 4 | 5 | [report] 6 | show_missing = True 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | - package-ecosystem: "pip" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | -------------------------------------------------------------------------------- /.github/workflows/auto-review.yml: -------------------------------------------------------------------------------- 1 | # This is a single-maintainer project but I want to require reviews before 2 | # merge, which means that I need a bot to review my own work. 3 | name: Automatic pull request approvals 4 | on: 5 | pull_request_target: 6 | types: 7 | - opened 8 | - reopened 9 | - synchronize 10 | - ready_for_review 11 | check_suite: 12 | types: 13 | - completed 14 | jobs: 15 | auto-approve: 16 | runs-on: ubuntu-latest 17 | if: > 18 | github.event.pull_request.head.repo.full_name == github.repository && 19 | github.event.pull_request.draft == false && ( 20 | github.event.action == 'opened' || 21 | github.event.action == 'reopened' || 22 | github.event.action == 'synchronize' 23 | ) && ( 24 | github.actor == 'jaqx0r' 25 | ) 26 | permissions: 27 | # wait on check 28 | checks: read 29 | # create review 30 | pull-requests: write 31 | steps: 32 | - uses: lewagon/wait-on-check-action@v1.3.4 33 | with: 34 | ref: ${{ github.event.pull_request.head.sha }} 35 | repo-token: ${{ github.token }} 36 | check-regexp: "test.*" 37 | wait-interval: 60 38 | 39 | - uses: "actions/github-script@v7" 40 | with: 41 | github-token: ${{ github.token }} 42 | script: | 43 | await github.rest.pulls.createReview({ 44 | event: "APPROVE", 45 | owner: context.repo.owner, 46 | pull_number: context.payload.pull_request.number, 47 | repo: context.repo.repo, 48 | }) 49 | -------------------------------------------------------------------------------- /.github/workflows/automerge.yml: -------------------------------------------------------------------------------- 1 | # We "trust" dependabot updates once they pass tests. 2 | # (this still requires all other checks to pass!) 3 | 4 | # This doesn't work on forked repos per the discussion in 5 | # https://github.com/pascalgn/automerge-action/issues/46 so don't attempt to 6 | # add people other than dependabot to the if field below. 7 | name: dependabot-auto-merge 8 | on: 9 | pull_request_target: 10 | types: 11 | # Dependabot will label the PR 12 | - labeled 13 | # Dependabot has rebased the PR 14 | - synchronize 15 | 16 | jobs: 17 | enable-automerge: 18 | if: github.event.pull_request.user.login == 'dependabot[bot]' && contains(github.event.pull_request.labels.*.name, 'dependencies') 19 | runs-on: ubuntu-latest 20 | permissions: 21 | # enable-automerge is a graphql query, not REST, so isn't documented, 22 | # except in a mention in 23 | # https://github.blog/changelog/2021-02-04-pull-request-auto-merge-is-now-generally-available/ 24 | # which says "can only be enabled by users with permissino to merge"; the 25 | # REST documentation says you need contents: write to perform a merge. 26 | # https://github.community/t/what-permission-does-a-github-action-need-to-call-graphql-enablepullrequestautomerge/197708 27 | # says this is it 28 | contents: write 29 | steps: 30 | # Enable auto-merge *before* issuing an approval. 31 | - uses: alexwilson/enable-github-automerge-action@main 32 | with: 33 | github-token: "${{ secrets.GITHUB_TOKEN }}" 34 | 35 | wait-on-checks: 36 | needs: enable-automerge 37 | runs-on: ubuntu-latest 38 | permissions: 39 | # wait-on-check requires only checks read 40 | checks: read 41 | steps: 42 | - uses: lewagon/wait-on-check-action@v1.3.4 43 | with: 44 | ref: ${{ github.event.pull_request.head.sha }} 45 | check-regexp: "test.*" 46 | repo-token: ${{ secrets.GITHUB_TOKEN }} 47 | wait-interval: 60 48 | 49 | approve: 50 | needs: wait-on-checks 51 | runs-on: ubuntu-latest 52 | permissions: 53 | # https://github.com/hmarr/auto-approve-action/issues/183 says 54 | # auto-approve-action requires write on pull-requests 55 | pull-requests: write 56 | steps: 57 | - uses: hmarr/auto-approve-action@f0939ea97e9205ef24d872e76833fa908a770363 58 | with: 59 | github-token: "${{ secrets.GITHUB_TOKEN }}" 60 | -------------------------------------------------------------------------------- /.github/workflows/ci-done.yml: -------------------------------------------------------------------------------- 1 | name: Comment CI test results on PR 2 | on: 3 | workflow_run: 4 | workflows: ["CI"] 5 | types: 6 | - completed 7 | jobs: 8 | comment: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | # list and download 12 | actions: read 13 | # post results as comment 14 | pull-requests: write 15 | # publish creates a check run 16 | checks: write 17 | steps: 18 | - uses: actions/github-script@v7 19 | with: 20 | script: | 21 | var artifacts = await github.rest.actions.listWorkflowRunArtifacts({ 22 | owner: context.repo.owner, 23 | repo: context.repo.repo, 24 | run_id: ${{github.event.workflow_run.id }}, 25 | }); 26 | var matchArtifact = artifacts.data.artifacts.filter((artifact) => { 27 | return artifact.name == "test-results" 28 | })[0]; 29 | var download = await github.rest.actions.downloadArtifact({ 30 | owner: context.repo.owner, 31 | repo: context.repo.repo, 32 | artifact_id: matchArtifact.id, 33 | archive_format: 'zip', 34 | }); 35 | var fs = require('fs'); 36 | fs.writeFileSync('${{github.workspace}}/test-results.zip', Buffer.from(download.data)); 37 | - id: unpack 38 | run: | 39 | mkdir -p test-results 40 | unzip -d test-results test-results.zip 41 | echo "sha=$(cat test-results/sha-number)" >> "$GITHUB_OUTPUT" 42 | - uses: docker://ghcr.io/enricomi/publish-unit-test-result-action:v1.6 43 | with: 44 | commit: ${{ steps.unpack.outputs.sha }} 45 | check_name: Unit Test Results 46 | github_token: ${{ secrets.GITHUB_TOKEN }} 47 | files: "**/test-results/**/*.xml" 48 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | branches: 7 | - main 8 | pull_request: 9 | 10 | permissions: 11 | # none-all, which doesn't exist, but 12 | # https://docs.github.com/en/actions/reference/authentication-in-a-workflow#using-the-github_token-in-a-workflow 13 | # implies that the token still gets created. Elsewhere we learn that any 14 | # permission not mentioned here gets turned to `none`. 15 | actions: none 16 | 17 | jobs: 18 | test: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: actions/setup-python@v5.6.0 23 | with: 24 | cache: 'pip' 25 | - name: install dependencies 26 | run: | 27 | sudo apt-get update -y 28 | sudo apt-get install -y libnss-db libdb-dev libcurl4-gnutls-dev libgnutls28-dev libldap2-dev libsasl2-dev 29 | python -m pip install --upgrade pip 30 | pip install -r requirements.txt 31 | pip install pytest-github-actions-annotate-failures 32 | - name: Test 33 | run: | 34 | mkdir -p test-results 35 | if [[ ${{ github.event_name }} == 'pull_request' ]]; then 36 | echo ${{ github.event.pull_request.head.sha }} > test-results/sha-number 37 | else 38 | echo ${{ github.sha }} > test-results/sha-number 39 | fi 40 | python setup.py test --addopts "-v --durations=0 --junitxml=test-results/junit.xml --cov=nss_cache" 41 | - uses: codecov/codecov-action@v5 42 | if: always() 43 | - name: Install 44 | run: pip install --user . 45 | - name: slapd Regression Test 46 | run: | 47 | sudo apt-get install -y slapd ldap-utils libnss-db db-util 48 | tests/slapd-regtest 49 | - uses: actions/upload-artifact@v4 50 | if: always() 51 | with: 52 | name: test-results 53 | path: test-results/ 54 | - name: pylint 55 | run: | 56 | pip install pylint 57 | pylint nsscache nss_cache 58 | # TODO(jaq): eventually make this lint clean and remove this line 59 | continue-on-error: true 60 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '22 12 * * 1' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'python' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v4 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v3 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v3 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v3 68 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | branches: 8 | - main 9 | pull_request: 10 | 11 | permissions: 12 | # none-all, which doesn't exist, but 13 | # https://docs.github.com/en/actions/reference/authentication-in-a-workflow#using-the-github_token-in-a-workflow 14 | # implies that the token still gets created. Elsewhere we learn that any 15 | # permission not mentioned here gets turned to `none`. 16 | actions: none 17 | 18 | jobs: 19 | black: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: psf/black@stable 24 | with: 25 | version: "~= 22.0" 26 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | # Test that this workflow parses on PR 5 | pull_request: 6 | push: 7 | tags: 8 | - v* 9 | - version/* 10 | 11 | permissions: 12 | # writes to the Releases API 13 | contents: write 14 | 15 | jobs: 16 | release: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: softprops/action-gh-release@v2 21 | # Only execute on a tag 22 | if: startsWith(github.ref, 'refs/tags/') 23 | with: 24 | generate_release_notes: true 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | MANIFEST 2 | build 3 | debian/files 4 | debian/nsscache.debhelper.log 5 | debian/nsscache.postinst.debhelper 6 | debian/nsscache.prerm.debhelper 7 | debian/nsscache.substvars 8 | debian/nsscache 9 | debian/patches 10 | dist 11 | __pycache__/ 12 | .pc 13 | *.pyc 14 | *.dsc 15 | *.tar.gz 16 | *.deb 17 | *.changes 18 | *.upload 19 | *.diff.gz 20 | *.build 21 | a.out 22 | *.debian.tar.xz 23 | .pybuild 24 | debian/debhelper-build-stamp 25 | *~ 26 | debian/.debhelper/ 27 | /.cache/ 28 | /.eggs/ 29 | /.ghi.yml 30 | /.pytest_cache/ 31 | nsscache.egg-info/ 32 | /tmpconfig.yml 33 | /.coverage 34 | /test-results/ 35 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at jaq@google.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Want to contribute? Great! First, read this page (including the small print at the end). 2 | 3 | ### Before you contribute 4 | Before we can use your code, you must sign the 5 | [Google Individual Contributor License Agreement](https://developers.google.com/open-source/cla/individual?csw=1) 6 | (CLA), which you can do online. The CLA is necessary mainly because you own the 7 | copyright to your changes, even after your contribution becomes part of our 8 | codebase, so we need your permission to use and distribute your code. We also 9 | need to be sure of various other things—for instance that you'll tell us if you 10 | know that your code infringes on other people's patents. You don't have to sign 11 | the CLA until after you've submitted your code for review and a member has 12 | approved it, but you must do it before we can put your code into our codebase. 13 | Before you start working on a larger contribution, you should get in touch with 14 | us first through the issue tracker with your idea so that we can help out and 15 | possibly guide you. Coordinating up front makes it much easier to avoid 16 | frustration later on. 17 | 18 | ### Code reviews 19 | All submissions, including submissions by project members, require review. We 20 | use Github pull requests for this purpose. 21 | 22 | Please format your code with github.com/google/yapf before sending pull 23 | requests. You can install this from PyPI with `pip install yapf` or on Debian 24 | systems as the `yapf3` package. 25 | 26 | ### Response Time 27 | 28 | This repository is maintained as a best effort service. 29 | 30 | Response times to issues and PRs may vary with the availability of the 31 | maintainers. We appreciate your patience. 32 | 33 | PRs with unit tests will be merged promptly. All other requests (issues and 34 | PRs) may take longer to be responded to. 35 | 36 | ### The small print 37 | Contributions made by corporations are covered by a different agreement than 38 | the one above, the Software Grant and Corporate Contributor License Agreement. 39 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10 2 | RUN apt-get update 3 | ENV DEBIAN_FRONTEND=noninteractive 4 | RUN apt-get install -y libsasl2-dev libldap2-dev libssl-dev slapd ldap-utils 5 | 6 | ENV VIRTUAL_ENV=/opt/venv 7 | RUN python -m venv $VIRTUAL_ENV 8 | ENV PATH=$VIRTUAL_ENV/bin:$PATH 9 | 10 | WORKDIR /code 11 | 12 | ADD ./requirements.txt /code/requirements.txt 13 | RUN pip install -r requirements.txt 14 | 15 | ADD . /code 16 | RUN python setup.py test 17 | RUN python setup.py install 18 | RUN tests/slapd-regtest 19 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include nsscache.conf 2 | include nsscache.conf.5 3 | include nsscache.1 4 | include nsscache.cron 5 | include nsscache.spec 6 | include COPYING 7 | include THANKS 8 | include runtests.py 9 | include examples/* 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | ### 3 | ## CircleCI development targets 4 | # 5 | 6 | .PHONY: circleci-validate 7 | circleci-validate: .circleci/config.yml 8 | circleci config validate 9 | 10 | # Override this on the make command to say which job to run 11 | CIRCLEJOB ?= build 12 | .PHONY: circleci-execute 13 | .INTERMEDIATE: tmpconfig.yml 14 | circleci-execute: .circleci/config.yml circleci-validate 15 | ifeq ($(CIRCLECI),true) 16 | $(error "Don't run this target from within CircleCI!") 17 | endif 18 | circleci config process $< > tmpconfig.yml 19 | circleci local execute -c tmpconfig.yml --job $(CIRCLEJOB) 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | nsscache - Asynchronously synchronise local NSS databases with remote directory services 2 | ======================================================================================== 3 | 4 | ![ci](https://github.com/google/nsscache/workflows/CI/badge.svg) 5 | [![codecov](https://codecov.io/gh/google/nsscache/branch/master/graph/badge.svg)](https://codecov.io/gh/google/nsscache) 6 | 7 | *nsscache* is a commandline tool and Python library that synchronises a local NSS cache from a remote directory service, such as LDAP. 8 | 9 | As soon as you have more than one machine in your network, you want to share usernames between those systems. Linux administrators have been brought up on the convention of LDAP or NIS as a directory service, and `/etc/nsswitch.conf`, `nss_ldap.so`, and `nscd` to manage their nameservice lookups. 10 | 11 | Even small networks will have experienced intermittent name lookup failures, such as a mail receiver sometimes returning "User not found" on a mailbox destination because of a slow socket over a congested network, or erratic cache behaviour by `nscd`. To combat this problem, we have separated the network from the NSS lookup codepath, by using an asynchronous cron job and a glorified script, to improve the speed and reliability of NSS lookups. We [presented at linux.conf.au 2008](https://mirror.linux.org.au/pub/linux.conf.au/2008/Wed/mel8-056.ogg), ([PDF slides](https://mirror.linux.org.au/pub/linux.conf.au/2008/slides/056-posix-jaq-v.pdf)) on the problems in NSS and the requirements for a solution. 12 | 13 | Here, we present to you this glorified script, which is just a little more extensible than 14 | 15 | ldapsearch | awk > /etc/passwd 16 | 17 | Read the [Google Code blog announcement](http://www.anchor.com.au/blog/2009/02/nsscache-and-ldap-reliability/) for nsscache, or more about the [motivation behind this tool](https://github.com/google/nsscache/wiki/MotivationBehindNssCache). 18 | 19 | Here's a [testimonial from Anchor Systems](http://www.anchor.com.au/blog/2009/02/nsscache-and-ldap-reliability/) on their deployment of nsscache. 20 | 21 | 22 | Pair *nsscache* with https://github.com/google/libnss-cache to integrate the local cache with your name service switch. 23 | 24 | --- 25 | 26 | Mailing list: https://groups.google.com/forum/#!forum/nsscache-discuss 27 | 28 | Issue history is at https://code.google.com/p/nsscache/issues/list 29 | 30 | --- 31 | 32 | # Contributions 33 | 34 | Please format your code with https://github.com/google/yapf (installable as `pip install yapf` or the `yapf3` package on Debian systems) before sending pull requests. 35 | 36 | # Testing 37 | 38 | The [`Dockerfile`](Dockerfile) sets up a container that then executes the python unit tests and [`tests/slapd-regtest`](tests/slapd-regtest) integration test. Execute that with `podman build .` to get a reproducible test environment. 39 | 40 | The `Dockerfile` mimics the test environment used by the Github Actions workflow [`.github/workflows/ci.yml`](.github/workflows/ci.yml) 41 | 42 | # Setup 43 | 44 | ## `gcs` source 45 | 46 | Install 47 | [Google Cloud Storage Python Client](https://cloud.google.com/python/docs/reference/storage/latest): 48 | `sudo pip install google-cloud-storage` 49 | 50 | For Compute Engine Instances to use the `gcs` source, their attached service 51 | account must have the _Storage Object Viewer_ role on the GCS bucket storing 52 | the `passwd`, `group`, and `shadow` objects, or on the objects themselves 53 | if using find-grained access controls. 54 | -------------------------------------------------------------------------------- /THANKS: -------------------------------------------------------------------------------- 1 | These people have helped improve nsscache by providing patches, filing 2 | bugs, etc. 3 | 4 | Christian Marie (pingu) 5 | kamil.kisiel 6 | Berend De Schouwer 7 | huw.lynes 8 | Robin H. Johnson 9 | antarus@google.com 10 | albibek@gmail.com 11 | javi@trackuino.org 12 | Jesse W. Hathaway 13 | jmartinj@ies1libertas.es 14 | Robert Flemming 15 | Jeff Bailey 16 | ohookins@gmail.com 17 | mimianddaniel@gmail.com 18 | Kevin Bowling 19 | Joshua Pereyda 20 | -------------------------------------------------------------------------------- /debian/README.Debian: -------------------------------------------------------------------------------- 1 | README.Debian for nsscache 2 | ========================== 3 | 4 | To complete installation of nsscache: 5 | 6 | * Configure /etc/nsscache.conf 7 | 8 | A basic configuration is given. You will want to modify the LDAP base 9 | as appropriate for your site. 10 | 11 | * Run `nsscache update' once. The map caches will be built per the 12 | configuration. 13 | 14 | * Reconfigure /etc/nsswitch.conf for your new maps. 15 | 16 | Append `db' to each of the maps you are configured for. 17 | 18 | E.g.: 19 | passwd = files db 20 | shadow = files db 21 | group = files db 22 | 23 | Replace `ldap' if you are no longer using that map (recommended). 24 | 25 | -- Jamie Wilkinson , 2007-04-02 26 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 9 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: nsscache 2 | Section: admin 3 | Priority: optional 4 | Maintainer: Jamie Wilkinson 5 | Build-Depends: debhelper (>= 9~), python3, dh-python, python3-pycurl, python3-ldap, python3-mox3, tzdata, python3-pytest-runner, python3-pytest, python3-boto3 6 | Standards-Version: 4.1.1 7 | Homepage: https://github.com/google/nsscache 8 | Vcs-Browser: https://github.com/google/nsscache/tree/debian 9 | Vcs-Git: https://github.com/google/nsscache.git -b debian 10 | 11 | Package: nsscache 12 | Architecture: all 13 | Depends: ${shlibs:Depends}, ${misc:Depends}, ${python3:Depends}, python3-pycurl, python3-ldap 14 | Provides: ${python3:Provides} 15 | Recommends: libnss-cache 16 | Suggests: python3-boto3 17 | Description: asynchronously synchronise local NSS databases with remote directory services 18 | Synchronises local NSS caches, such as those served by the 19 | libnss-cache module, against remote directory services, such as 20 | LDAP, or prebuild cache files from an HTTP server. This can be 21 | used alongside the libnss-cache package to keep user account 22 | information, groups, netgroups, and automounts up to date. 23 | . 24 | Use of nsscache and libnss-cache eliminates the need for using a 25 | cache daemon such as nscd with networked NSS modules such as 26 | libnss-ldap. 27 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | This package was debianized by Jamie Wilkinson on 2 | Mon, 19 Mar 2007 09:54:10 +1000. 3 | 4 | Copyright: 5 | 6 | Copyright 2007-2011 Google, Inc. 7 | 8 | License: 9 | 10 | This package is free software; you can redistribute it and/or modify 11 | it under the terms of the GNU General Public License as published by 12 | the Free Software Foundation; either version 2 of the License, or 13 | (at your option) any later version. 14 | 15 | This package is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU General Public License for more details. 19 | 20 | You should have received a copy of the GNU General Public License 21 | along with this package; if not, write to the Free Software 22 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 23 | 24 | On Debian systems, the complete text of the GNU General 25 | Public License can be found in `/usr/share/common-licenses/GPL'. 26 | -------------------------------------------------------------------------------- /debian/dirs: -------------------------------------------------------------------------------- 1 | usr/sbin 2 | var/lib/nsscache 3 | -------------------------------------------------------------------------------- /debian/examples: -------------------------------------------------------------------------------- 1 | nsscache.conf 2 | nsscache.cron 3 | examples/authorized-keys-command.sh 4 | -------------------------------------------------------------------------------- /debian/gbp.conf: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | upstream-tag = version/%(version)s 3 | debian-branch=debian 4 | -------------------------------------------------------------------------------- /debian/links: -------------------------------------------------------------------------------- 1 | /usr/bin/nsscache /usr/sbin/nsscache 2 | -------------------------------------------------------------------------------- /debian/nsscache.cron: -------------------------------------------------------------------------------- 1 | # /etc/cron.d/nsscache: crontab entries for the nsscache package 2 | 3 | SHELL=/bin/sh 4 | PATH=/usr/bin 5 | MAILTO=root 6 | 7 | # update the cache 15 minutely 8 | %MINUTE15%/15 * * * * root /usr/bin/nsscache update 9 | 10 | # perform a full update once a day, at a time chosen during package 11 | # configuration (between 2AM and 5AM) 12 | %MINUTE% %HOUR% * * * root /usr/bin/nsscache update --full 13 | -------------------------------------------------------------------------------- /debian/nsscache.manpages: -------------------------------------------------------------------------------- 1 | nsscache.1 2 | nsscache.conf.5 3 | -------------------------------------------------------------------------------- /debian/pybuild.testfiles: -------------------------------------------------------------------------------- 1 | nsscache.conf 2 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | # Uncomment this to turn on verbose mode. 4 | export DH_VERBOSE=1 5 | export PYBUILD_NAME=nsscache 6 | export PYBUILD_TEST_PYTEST=1 7 | export PYBUILD_TEST_ARGS= 8 | 9 | %: 10 | dh $@ --with=python3 --buildsystem=pybuild 11 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (quilt) 2 | -------------------------------------------------------------------------------- /debian/source/options: -------------------------------------------------------------------------------- 1 | single-debian-patch 2 | # Ignore files not included in release tarball. 3 | extend-diff-ignore = "(^|/)(MANIFEST(\.in)?|rpm/.*|dist/.*|build/.*|.eggs/.*|.pytest_cache/.*)$" 4 | -------------------------------------------------------------------------------- /debian/tests/control: -------------------------------------------------------------------------------- 1 | Tests: slapd-regtest 2 | Restrictions: allow-stderr 3 | Depends: @, 4 | slapd, 5 | ldap-utils, 6 | db-util 7 | -------------------------------------------------------------------------------- /debian/tests/slapd-regtest: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -x 4 | 5 | if [[ -z ${ADTTMP-} ]]; then 6 | WORKDIR=$(mktemp -d -t nsscache.regtest.XXXXXX) 7 | ARTIFACTS=${WORKDIR} 8 | else 9 | WORKDIR=${ADTTMP} 10 | ARTIFACTS=${ADT_ARTIFACTS} 11 | fi 12 | 13 | 14 | export WORKDIR ARTIFACTS 15 | 16 | 17 | 18 | cleanup() { 19 | if [[ -e "$WORKDIR/slapd.pid" ]]; then 20 | kill -TERM $(cat $WORKDIR/slapd.pid) 21 | fi 22 | if [[ -z ${ADTTMP-} ]]; then 23 | rm -rf $WORKDIR 24 | fi 25 | } 26 | 27 | trap cleanup 0 INT QUIT ABRT PIPE TERM 28 | 29 | ../../tests/slapd-regtest 30 | -------------------------------------------------------------------------------- /debian/watch: -------------------------------------------------------------------------------- 1 | version=3 2 | opts=filenamemangle=s/.+\/v?(\d\S*)\.tar\.gz/nsscache-$1\.tar\.gz/ \ 3 | https://github.com/google/nsscache/tags .*/version\/(\d\S*)\.tar\.gz 4 | -------------------------------------------------------------------------------- /doc/mox_to_mock.md: -------------------------------------------------------------------------------- 1 | # Translating pymox to unittest.mock 2 | 3 | ## Creating a mock object 4 | 5 | self.mox.CreateMock({}) -> mock.create_autospec({}) 6 | 7 | ## Creating a mock anything 8 | 9 | self.mox.CreateMockAnything() ->> mock.Mock() 10 | 11 | ## Matching arguments and mockign return value 12 | 13 | {}.fn(args...).AndReturn(ret) -> 14 | 15 | {}.fn.return_value = ret 16 | 17 | ... after act phase 18 | 19 | {}.fn.assert_called_with(args) 20 | 21 | ## Replacing attributes with mocks 22 | 23 | self.mox.StubOutWithMock(a, b) -> 24 | 25 | a.b = mock.Mock() 26 | 27 | ## Replacing class with mocks 28 | 29 | self.moxk.StubOutClassWithMocks(a, b) -> 30 | 31 | @mock.patch.object(a, b) 32 | 33 | 34 | ## Ignoring arguments 35 | 36 | mox.IgnoreArg() -> mock.ANY 37 | 38 | ## Multiple return values 39 | 40 | multiple return values -> side_effect=[...] 41 | 42 | ## Raising exceptions 43 | 44 | {}.AndRaise(r) -> {}.side_effect=r 45 | -------------------------------------------------------------------------------- /examples/authorized-keys-command.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # This script returns one or more authorized keys for use by SSH, by extracting 4 | # them from a local cache file /etc/sshkey.cache. 5 | # 6 | # Ensure this script is mentioned in the sshd_config like so: 7 | # 8 | # AuthorizedKeysCommand /path/to/nsscache/authorized-keys-command.sh 9 | 10 | awk -F: -v name="$1" '$0 ~ name {print $2}' /etc/sshkey.cache | \ 11 | tr -d "[']" | \ 12 | sed -e 's/, /\n/g' 13 | -------------------------------------------------------------------------------- /nss_cache/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/nsscache/66e3789910b2641e52707bba55d6e5d381069257/nss_cache/.DS_Store -------------------------------------------------------------------------------- /nss_cache/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2007 Google Inc. 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | """Library for client side caching of NSS data. 17 | 18 | The nsscache package implements client-side caching of nss data 19 | from various sources to different local nss storage implementations. 20 | 21 | This file all the availible known caches, maps, and sources for the 22 | nss_cache package. 23 | """ 24 | 25 | __author__ = ( 26 | "jaq@google.com (Jamie Wilkinson)", 27 | "vasilios@google.com (Vasilios Hoffman)", 28 | ) 29 | 30 | __version__ = "0.48" 31 | -------------------------------------------------------------------------------- /nss_cache/app_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2007 Google Inc. 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | """Unit tests for nss_cache/app.py.""" 17 | 18 | __author__ = "vasilios@google.com (Vasilios Hoffman)" 19 | 20 | import logging 21 | import io 22 | import os 23 | import sys 24 | import unittest 25 | 26 | from nss_cache import app 27 | 28 | 29 | class TestNssCacheApp(unittest.TestCase): 30 | """Unit tests for NssCacheApp class.""" 31 | 32 | def setUp(self): 33 | dev_null = io.StringIO() 34 | self.stdout = sys.stdout 35 | sys.stdout = dev_null 36 | self.srcdir = os.path.normpath(os.path.join(os.path.dirname(__file__), "..")) 37 | self.conf_filename = os.path.join(self.srcdir, "nsscache.conf") 38 | 39 | def tearDown(self): 40 | sys.stdout = self.stdout 41 | 42 | def testRun(self): 43 | return_code = app.NssCacheApp().Run([], {}) 44 | self.assertEqual(os.EX_USAGE, return_code) 45 | 46 | def testParseGlobalOptions(self): 47 | a = app.NssCacheApp() 48 | (options, args) = a.parser.parse_args(["-d", "-v", "command"]) 49 | self.assertNotEqual(None, options.debug) 50 | self.assertNotEqual(None, options.verbose) 51 | self.assertEqual(["command"], args) 52 | 53 | def testParseCommandLineDebug(self): 54 | a = app.NssCacheApp() 55 | (options, args) = a.parser.parse_args(["-d"]) 56 | self.assertNotEqual(None, options.debug) 57 | (options, args) = a.parser.parse_args(["--debug"]) 58 | self.assertNotEqual(None, options.debug) 59 | a.Run(["-d"], {}) 60 | self.assertEqual(logging.DEBUG, a.log.getEffectiveLevel()) 61 | 62 | def testParseCommandLineVerbose(self): 63 | a = app.NssCacheApp() 64 | (options, args) = a.parser.parse_args(["-v"]) 65 | self.assertNotEqual(None, options.verbose) 66 | self.assertEqual([], args) 67 | (options, args) = a.parser.parse_args(["--verbose"]) 68 | self.assertNotEqual(None, options.verbose) 69 | self.assertEqual([], args) 70 | a.Run(["-v"], {}) 71 | self.assertEqual(logging.INFO, a.log.getEffectiveLevel()) 72 | 73 | def testParseCommandLineVerboseDebug(self): 74 | a = app.NssCacheApp() 75 | a.Run(["-v", "-d"], {}) 76 | self.assertEqual(logging.DEBUG, a.log.getEffectiveLevel()) 77 | 78 | def testParseCommandLineConfigFile(self): 79 | a = app.NssCacheApp() 80 | (options, args) = a.parser.parse_args(["-c", "file"]) 81 | self.assertNotEqual(None, options.config_file) 82 | self.assertEqual([], args) 83 | (options, args) = a.parser.parse_args(["--config-file", "file"]) 84 | self.assertNotEqual(None, options.config_file) 85 | self.assertEqual([], args) 86 | 87 | def testBadOptionsCauseNoExit(self): 88 | a = app.NssCacheApp() 89 | stderr_buffer = io.StringIO() 90 | old_stderr = sys.stderr 91 | sys.stderr = stderr_buffer 92 | self.assertEqual(2, a.Run(["--invalid"], {})) 93 | sys.stderr = old_stderr 94 | 95 | def testHelpOptionPrintsGlobalHelp(self): 96 | stdout_buffer = io.StringIO() 97 | a = app.NssCacheApp() 98 | old_stdout = sys.stdout 99 | sys.stdout = stdout_buffer 100 | self.assertEqual(0, a.Run(["--help"], {})) 101 | sys.stdout = old_stdout 102 | self.assertNotEqual(0, stdout_buffer.tell()) 103 | (prelude, usage, commands, options) = stdout_buffer.getvalue().split("\n\n") 104 | self.assertTrue(prelude.startswith("nsscache synchronises")) 105 | expected_str = "Usage: nsscache [global options] command [command options]" 106 | self.assertEqual(expected_str, usage) 107 | self.assertTrue(commands.startswith("commands:")) 108 | self.assertTrue(options.startswith("Options:")) 109 | self.assertTrue(options.find("show this help message and exit") >= 0) 110 | 111 | def testHelpCommandOutput(self): 112 | # trap stdout into a StringIO 113 | stdout_buffer = io.StringIO() 114 | a = app.NssCacheApp() 115 | old_stdout = sys.stdout 116 | sys.stdout = stdout_buffer 117 | self.assertEqual(0, a.Run(["help"], {})) 118 | sys.stdout = old_stdout 119 | self.assertNotEqual(0, stdout_buffer.tell()) 120 | self.assertTrue(stdout_buffer.getvalue().find("nsscache synchronises") >= 0) 121 | 122 | def testRunBadArgsPrintsGlobalHelp(self): 123 | # trap stdout into a StringIO 124 | stdout_buffer = io.StringIO() 125 | old_stdout = sys.stdout 126 | sys.stdout = stdout_buffer 127 | # verify bad arguments calls help 128 | return_code = app.NssCacheApp().Run( 129 | ["blarg"], {"NSSCACHE_CONFIG": self.conf_filename} 130 | ) 131 | sys.stdout = old_stdout 132 | assert return_code == 70 # EX_SOFTWARE 133 | assert stdout_buffer.getvalue().find("enable debugging") >= 0 134 | 135 | 136 | if __name__ == "__main__": 137 | unittest.main() 138 | -------------------------------------------------------------------------------- /nss_cache/caches/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/nsscache/66e3789910b2641e52707bba55d6e5d381069257/nss_cache/caches/__init__.py -------------------------------------------------------------------------------- /nss_cache/caches/cache_factory.py: -------------------------------------------------------------------------------- 1 | # Copyright 2007 Google Inc. 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | """Package level factory implementation for cache implementations. 17 | 18 | We use a factory instead of relying on the __init__.py module to 19 | register cache implementations at import time. This is much more 20 | reliable. 21 | """ 22 | 23 | __author__ = "springer@google.com (Matthew Springer)" 24 | 25 | import logging 26 | 27 | from nss_cache.caches import files 28 | 29 | _cache_implementations = {} 30 | 31 | 32 | def RegisterImplementation(cache_name, map_name, cache): 33 | """Register a Cache implementation with the CacheFactory. 34 | 35 | Child modules are expected to call this method in the file-level scope 36 | so that the CacheFactory is aware of them. 37 | 38 | Args: 39 | cache_name: (string) The name of the NSS backend. 40 | map_name: (string) The name of the map handled by this Cache. 41 | cache: A class type that is a subclass of Cache. 42 | 43 | Returns: Nothing 44 | """ 45 | global _cache_implementations 46 | if cache_name not in _cache_implementations: 47 | logging.info("Registering [%s] cache for [%s].", cache_name, map_name) 48 | _cache_implementations[cache_name] = {} 49 | _cache_implementations[cache_name][map_name] = cache 50 | 51 | 52 | def Create(conf, map_name, automount_mountpoint=None): 53 | """Cache creation factory method. 54 | 55 | Args: 56 | conf: a dictionary of configuration key/value pairs, including one 57 | required attribute 'name' 58 | map_name: a string identifying the map name to handle 59 | automount_mountpoint: A string containing the automount mountpoint, used only 60 | by automount maps. 61 | 62 | Returns: 63 | an instance of a Cache 64 | 65 | Raises: 66 | RuntimeError: problem instantiating the requested cache 67 | """ 68 | global _cache_implementations 69 | if not _cache_implementations: 70 | raise RuntimeError("no cache implementations exist") 71 | cache_name = conf["name"] 72 | 73 | if cache_name not in _cache_implementations: 74 | raise RuntimeError("cache not implemented: %r" % (cache_name,)) 75 | if map_name not in _cache_implementations[cache_name]: 76 | raise RuntimeError("map %r not supported by cache %r" % (map_name, cache_name)) 77 | 78 | return _cache_implementations[cache_name][map_name]( 79 | conf, map_name, automount_mountpoint=automount_mountpoint 80 | ) 81 | 82 | 83 | files.RegisterAllImplementations(RegisterImplementation) 84 | -------------------------------------------------------------------------------- /nss_cache/caches/cache_factory_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2007 Google Inc. 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | """Unit tests for out cache factory.""" 17 | 18 | __author__ = "springer@google.com (Matthew Springer)" 19 | 20 | import unittest 21 | 22 | from nss_cache.caches import caches 23 | from nss_cache.caches import cache_factory 24 | 25 | 26 | class TestCacheFactory(unittest.TestCase): 27 | def testRegister(self): 28 | class DummyCache(caches.Cache): 29 | pass 30 | 31 | old_cache_implementations = cache_factory._cache_implementations 32 | cache_factory._cache_implementations = {} 33 | cache_factory.RegisterImplementation("dummy", "dummy", DummyCache) 34 | self.assertEqual(1, len(cache_factory._cache_implementations)) 35 | self.assertEqual(1, len(cache_factory._cache_implementations["dummy"])) 36 | self.assertEqual( 37 | DummyCache, cache_factory._cache_implementations["dummy"]["dummy"] 38 | ) 39 | cache_factory._cache_implementations = old_cache_implementations 40 | 41 | def testCreateWithNoImplementations(self): 42 | old_cache_implementations = cache_factory._cache_implementations 43 | cache_factory._cache_implementations = {} 44 | self.assertRaises(RuntimeError, cache_factory.Create, {}, "map_name") 45 | cache_factory._cache_implementations = old_cache_implementations 46 | 47 | def testThatRegularImplementationsArePresent(self): 48 | self.assertEqual(len(cache_factory._cache_implementations), 1) 49 | 50 | 51 | if __name__ == "__main__": 52 | unittest.main() 53 | -------------------------------------------------------------------------------- /nss_cache/caches/caches_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2007 Google Inc. 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | """Unit tests for caches/caches.py.""" 17 | 18 | __author__ = "jaq@google.com (Jamie Wilkinson)" 19 | 20 | import os 21 | import platform 22 | import stat 23 | import tempfile 24 | import unittest 25 | from unittest import mock 26 | 27 | from nss_cache import config 28 | from nss_cache.caches import caches 29 | 30 | 31 | class FakeCacheCls(caches.Cache): 32 | 33 | CACHE_FILENAME = "shadow" 34 | 35 | def __init__(self, config, map_name): 36 | super(FakeCacheCls, self).__init__(config, map_name) 37 | 38 | def Write(self, map_data): 39 | return 0 40 | 41 | def GetCacheFilename(self): 42 | return os.path.join(self.output_dir, self.CACHE_FILENAME + ".test") 43 | 44 | 45 | class TestCls(unittest.TestCase): 46 | def setUp(self): 47 | self.workdir = tempfile.mkdtemp() 48 | self.config = {"dir": self.workdir} 49 | if platform.system() == "FreeBSD": 50 | # FreeBSD doesn't have a shadow file 51 | self.shadow = config.MAP_PASSWORD 52 | else: 53 | self.shadow = config.MAP_SHADOW 54 | 55 | def tearDown(self): 56 | os.rmdir(self.workdir) 57 | 58 | def testCopyOwnerMissing(self): 59 | expected = os.stat(os.path.join("/etc", self.shadow)) 60 | expected = stat.S_IMODE(expected.st_mode) 61 | cache = FakeCacheCls(config=self.config, map_name=self.shadow) 62 | cache._Begin() 63 | cache._Commit() 64 | data = os.stat(os.path.join(self.workdir, cache.GetCacheFilename())) 65 | self.assertEqual(expected, stat.S_IMODE(data.st_mode)) 66 | os.unlink(cache.GetCacheFilename()) 67 | 68 | def testCopyOwnerPresent(self): 69 | expected = os.stat(os.path.join("/etc/", self.shadow)) 70 | expected = stat.S_IMODE(expected.st_mode) 71 | cache = FakeCacheCls(config=self.config, map_name=self.shadow) 72 | cache._Begin() 73 | cache._Commit() 74 | data = os.stat(os.path.join(self.workdir, cache.GetCacheFilename())) 75 | self.assertEqual(expected, stat.S_IMODE(data.st_mode)) 76 | os.unlink(cache.GetCacheFilename()) 77 | 78 | 79 | class TestCache(unittest.TestCase): 80 | def testWriteMap(self): 81 | cache_map = caches.Cache({}, config.MAP_PASSWORD, None) 82 | with mock.patch.object(cache_map, "Write") as write, mock.patch.object( 83 | cache_map, "Verify" 84 | ) as verify, mock.patch.object(cache_map, "_Commit") as commit: 85 | write.return_value = "entries_written" 86 | verify.return_value = True 87 | self.assertEqual(0, cache_map.WriteMap("writable_map")) 88 | 89 | 90 | if __name__ == "__main__": 91 | unittest.main() 92 | -------------------------------------------------------------------------------- /nss_cache/error.py: -------------------------------------------------------------------------------- 1 | # Copyright 2007 Google Inc. 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | """Exception classes for nss_cache module.""" 17 | 18 | __author__ = "vasilios@google.com (Vasilios Hoffman)" 19 | 20 | 21 | class Error(Exception): 22 | """Base exception class for nss_cache.""" 23 | 24 | pass 25 | 26 | 27 | class CacheNotFound(Error): 28 | """Raised when a local cache is missing.""" 29 | 30 | pass 31 | 32 | 33 | class CacheInvalid(Error): 34 | """Raised when a cache is invalid.""" 35 | 36 | pass 37 | 38 | 39 | class CommandParseError(Error): 40 | """Raised when the command line fails to parse correctly.""" 41 | 42 | pass 43 | 44 | 45 | class ConfigurationError(Error): 46 | """Raised when there is a problem with configuration values.""" 47 | 48 | pass 49 | 50 | 51 | class EmptyMap(Error): 52 | """Raised when an empty map is discovered and one is not expected.""" 53 | 54 | pass 55 | 56 | 57 | class NoConfigFound(Error): 58 | """Raised when no configuration file is loaded.""" 59 | 60 | pass 61 | 62 | 63 | class PermissionDenied(Error): 64 | """Raised when nss_cache cannot access a resource.""" 65 | 66 | pass 67 | 68 | 69 | class UnsupportedMap(Error): 70 | """Raised when trying to use an unsupported map type.""" 71 | 72 | pass 73 | 74 | 75 | class InvalidMap(Error): 76 | """Raised when an invalid map is encountered.""" 77 | 78 | pass 79 | 80 | 81 | class SourceUnavailable(Error): 82 | """Raised when a source is unavailable.""" 83 | 84 | pass 85 | 86 | 87 | class InvalidMerge(Error): 88 | """An invalid merge was attempted.""" 89 | -------------------------------------------------------------------------------- /nss_cache/error_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2007 Google Inc. 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | """Unit tests for nss_cache/error.py.""" 17 | 18 | __author__ = "vasilios@google.com (Vasilios Hoffman)" 19 | 20 | import unittest 21 | 22 | from nss_cache import error 23 | 24 | 25 | class TestError(unittest.TestCase): 26 | """Unit tests for error.py.""" 27 | 28 | def testError(self): 29 | """We can throw an error.Error.""" 30 | 31 | class Ooops(object): 32 | """Raises error.Error.""" 33 | 34 | def __init__(self): 35 | raise error.Error 36 | 37 | self.assertRaises(error.Error, Ooops) 38 | 39 | def testCacheNotFound(self): 40 | """We can throw an error.CacheNotFound.""" 41 | 42 | class Ooops(object): 43 | """Raises error.CacheNotFound.""" 44 | 45 | def __init__(self): 46 | raise error.CacheNotFound 47 | 48 | self.assertRaises(error.CacheNotFound, Ooops) 49 | 50 | def testCommandParseError(self): 51 | """We can throw an error.CommandParseError.""" 52 | 53 | class Ooops(object): 54 | """Raises error.CommandParseError.""" 55 | 56 | def __init__(self): 57 | raise error.CommandParseError 58 | 59 | self.assertRaises(error.CommandParseError, Ooops) 60 | 61 | def testConfigurationError(self): 62 | """We can throw an error.ConfigurationError.""" 63 | 64 | class Ooops(object): 65 | """Raises error.ConfigurationError.""" 66 | 67 | def __init__(self): 68 | raise error.ConfigurationError 69 | 70 | self.assertRaises(error.ConfigurationError, Ooops) 71 | 72 | def testEmptyMap(self): 73 | """error.EmptyMap is raisable.""" 74 | 75 | def Kaboom(): 76 | raise error.EmptyMap 77 | 78 | self.assertRaises(error.EmptyMap, Kaboom) 79 | 80 | def testNoConfigFound(self): 81 | """We can throw an error.NoConfigFound.""" 82 | 83 | class Ooops(object): 84 | """Raises error.NoConfigFound.""" 85 | 86 | def __init__(self): 87 | raise error.NoConfigFound 88 | 89 | self.assertRaises(error.NoConfigFound, Ooops) 90 | 91 | def testPermissionDenied(self): 92 | """error.PermissionDenied is raisable.""" 93 | 94 | def Kaboom(): 95 | raise error.PermissionDenied 96 | 97 | self.assertRaises(error.PermissionDenied, Kaboom) 98 | 99 | def testUnsupportedMap(self): 100 | """We can throw an error.UnsupportedMap.""" 101 | 102 | class Ooops(object): 103 | """Raises error.UnsupportedMap.""" 104 | 105 | def __init__(self): 106 | raise error.UnsupportedMap 107 | 108 | self.assertRaises(error.UnsupportedMap, Ooops) 109 | 110 | def testSourceUnavailable(self): 111 | """We can throw an error.SourceUnavailable.""" 112 | 113 | class Ooops(object): 114 | """Raises error.SourceUnavailable.""" 115 | 116 | def __init__(self): 117 | raise error.SourceUnavailable 118 | 119 | self.assertRaises(error.SourceUnavailable, Ooops) 120 | 121 | 122 | if __name__ == "__main__": 123 | unittest.main() 124 | -------------------------------------------------------------------------------- /nss_cache/lock.py: -------------------------------------------------------------------------------- 1 | # Copyright 2007 Google Inc. 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | """Lock management for nss_cache module.""" 17 | 18 | __author__ = "vasilios@google.com (Vasilios Hoffman)" 19 | 20 | import errno 21 | import fcntl 22 | import logging 23 | import os 24 | import re 25 | import signal 26 | import stat 27 | import sys 28 | 29 | 30 | # It would be interesting to subclass mutex, but we don't need the 31 | # queueing functionality. 32 | class PidFile(object): 33 | """Interprocess locking via fcntl and a pid file. 34 | 35 | We use fcntl to manage locks between processes, as the kernel will 36 | release the lock when the process dies no matter what, so it works 37 | quite well. 38 | 39 | We store the pid in the file we use so that 3rd party programs, 40 | primarily small shell scripts, can easily see who has (or had) the 41 | lock via the stored pid. We don't clean the pid up on exit 42 | because most programs will have to check if the program is still 43 | running anyways. 44 | 45 | We can forcibly take a lock by deleting the file and re-creating 46 | it. When we do so, we check if the pid in the file is running and 47 | send it a SIGTERM *if and only if* it has a commandline with 48 | 'nsscache' somewhere in the string. 49 | 50 | We try to kill the process to avoid it completing after us and 51 | overwriting any changes. We check for 'nsscache' to avoid killing 52 | a re-used PID. We are not paranoid, we send the SIGTERM and 53 | assume it dies. 54 | 55 | WARNING: Use over NFS with *extreme* caution. fcntl locking can 56 | be configured to work, but your mileage can and will vary. 57 | """ 58 | 59 | STATE_DIR = "/var/run" 60 | PROC_DIR = "/proc" 61 | PROG_NAME = "nsscache" 62 | 63 | def __init__(self, filename=None, pid=None): 64 | """Initialize the PidFile object.""" 65 | self._locked = False 66 | self._file = None 67 | self.filename = filename 68 | self.pid = pid 69 | 70 | # Setup logging. 71 | self.log = logging.getLogger(__name__) 72 | 73 | if self.pid is None: 74 | self.pid = os.getpid() 75 | 76 | # If no filename is given, default to the basename we were 77 | # invoked with. 78 | if self.filename is None: 79 | basename = os.path.basename(sys.argv[0]) 80 | if not basename: 81 | # We were invoked from a python interpreter with 82 | # bad arguments, or otherwise loaded without sys.argv 83 | # being set. 84 | self.log.critical("Can not determine lock file name!") 85 | raise TypeError("missing required argument: filename") 86 | self.filename = "%s/%s" % (self.STATE_DIR, basename) 87 | 88 | self.log.debug("using %s for lock file", self.filename) 89 | 90 | def __del__(self): 91 | """Release our pid file on object destruction.""" 92 | if self.Locked(): 93 | self.Unlock() 94 | 95 | def _Open(self, filename=None): 96 | """Create our file and store the file object.""" 97 | if filename is None: 98 | filename = self.filename 99 | 100 | # We want to create this file if it doesn't exist, but 'w' 101 | # will truncate, so we use 'a+' and seek. We don't truncate 102 | # the file because we haven't tested if it is locked by 103 | # another program yet, this is done later by fcntl module. 104 | self._file = open(filename, "a+") 105 | self._file.seek(0) 106 | 107 | # Set permissions. 108 | os.chmod(filename, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH) 109 | 110 | def Lock(self, force=False): 111 | """Open our pid file and lock it. 112 | 113 | Args: 114 | force: optional flag to override the lock. 115 | Returns: 116 | True if successful 117 | False otherwise 118 | """ 119 | if self._file is None: 120 | # Open the file and trap permission denied. 121 | try: 122 | self._Open() 123 | except IOError as e: 124 | if e.errno == errno.EACCES: 125 | self.log.warning( 126 | "Permission denied opening lock file: %s", self.filename 127 | ) 128 | return False 129 | raise 130 | 131 | # Try to get the lock. 132 | return_val = False 133 | try: 134 | fcntl.lockf(self._file, fcntl.LOCK_EX | fcntl.LOCK_NB) 135 | return_val = True 136 | except IOError as e: 137 | if e.errno in [errno.EACCES, errno.EAGAIN]: 138 | # Catch the error raised when the file is locked. 139 | if not force: 140 | self.log.debug("%s already locked!", self.filename) 141 | return False 142 | else: 143 | # Otherwise re-raise it. 144 | raise 145 | 146 | # Check if we need to forcibly re-try the lock. 147 | if not return_val and force: 148 | self.log.debug("retrying lock.") 149 | # Try to kill the process with the lock. 150 | self.SendTerm() 151 | # Clear the lock. 152 | self.ClearLock() 153 | # Try to lock only once more -- else we might recurse forever! 154 | return self.Lock(force=False) 155 | 156 | # Store the pid. 157 | self._file.truncate() 158 | self._file.write("%s\n" % self.pid) 159 | self._file.flush() 160 | 161 | self.log.debug("successfully locked %s", self.filename) 162 | 163 | self._locked = True 164 | return return_val 165 | 166 | def SendTerm(self): 167 | """Send a SIGTERM to the process in the pidfile. 168 | 169 | We only send a SIGTERM if such a process exists and it has a 170 | commandline including the string 'nsscache'. 171 | """ 172 | # Grab the pid 173 | pid_content = self._file.read() 174 | try: 175 | pid = int(pid_content.strip()) 176 | except (AttributeError, ValueError) as e: 177 | self.log.warning( 178 | "Not sending TERM, could not parse pid file content: %r", pid_content 179 | ) 180 | return 181 | 182 | self.log.debug("retrieved pid %d" % pid) 183 | 184 | # Reset the filehandle just in case. 185 | self._file.seek(0) 186 | 187 | # By reading cmdline out of /proc we establish: 188 | # a) if a process with that pid exists. 189 | # b) what the command line is, to see if it included 'nsscache'. 190 | proc_path = "%s/%i/cmdline" % (self.PROC_DIR, pid) 191 | try: 192 | proc_file = open(proc_path, "r") 193 | except IOError as e: 194 | if e.errno == errno.ENOENT: 195 | self.log.debug("process does not exist, skipping signal.") 196 | return 197 | raise 198 | 199 | cmdline = proc_file.read() 200 | proc_file.close() 201 | 202 | # See if it matches our program name regex. 203 | cmd_re = re.compile(r".*%s" % self.PROG_NAME) 204 | if not cmd_re.match(cmdline): 205 | self.log.debug( 206 | "process is running but not %s, skipping signal", self.PROG_NAME 207 | ) 208 | return 209 | 210 | # Send a SIGTERM. 211 | self.log.debug("sending SIGTERM to %i", pid) 212 | os.kill(pid, signal.SIGTERM) 213 | 214 | # We are not paranoid about success, so we're done! 215 | return 216 | 217 | def ClearLock(self): 218 | """Delete the pid file to remove any locks on it.""" 219 | self.log.debug("clearing old pid file: %s", self.filename) 220 | self._file.close() 221 | self._file = None 222 | os.remove(self.filename) 223 | 224 | def Locked(self): 225 | """Return True if locked, False if not.""" 226 | return self._locked 227 | 228 | def Unlock(self): 229 | """Release our pid file.""" 230 | fcntl.lockf(self._file, fcntl.LOCK_UN) 231 | self._locked = False 232 | -------------------------------------------------------------------------------- /nss_cache/maps/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/nsscache/66e3789910b2641e52707bba55d6e5d381069257/nss_cache/maps/__init__.py -------------------------------------------------------------------------------- /nss_cache/maps/automount.py: -------------------------------------------------------------------------------- 1 | # Copyright 2007 Google Inc. 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | """An implementation of an automount map for nsscache. 17 | 18 | AutomountMap: An implementation of NSS automount maps based on the Map 19 | class. 20 | 21 | AutomountMapEntry: A automount map entry based on the MapEntry class. 22 | """ 23 | 24 | __author__ = "vasilios@google.com (Vasilios Hoffman)" 25 | 26 | from nss_cache.maps import maps 27 | 28 | 29 | class AutomountMap(maps.Map): 30 | """This class represents an NSS automount map. 31 | 32 | Map data is stored as a list of MapEntry objects, see the abstract 33 | class Map. 34 | """ 35 | 36 | def __init__(self, iterable=None): 37 | """Construct a AutomountMap object using optional iterable.""" 38 | super(AutomountMap, self).__init__(iterable) 39 | 40 | def Add(self, entry): 41 | """Add a new object, verify it is a AutomountMapEntry object.""" 42 | if not isinstance(entry, AutomountMapEntry): 43 | raise TypeError("Entry is not an AutomountMapEntry: %r" % entry) 44 | return super(AutomountMap, self).Add(entry) 45 | 46 | 47 | class AutomountMapEntry(maps.MapEntry): 48 | """This class represents NSS automount map entries.""" 49 | 50 | __slots__ = ("key", "location", "options") 51 | _KEY = "key" 52 | _ATTRS = ("key", "location", "options") 53 | 54 | def __init__(self, data=None): 55 | """Construct a AutomountMapEntry.""" 56 | self.key = None 57 | self.location = None 58 | self.options = None 59 | 60 | super(AutomountMapEntry, self).__init__(data) 61 | -------------------------------------------------------------------------------- /nss_cache/maps/automount_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2007 Google Inc. 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | """Unit tests for automount.py. 17 | 18 | We only test what is overridden in the automount subclasses, most 19 | functionality is in base.py and tested in passwd_test.py since a 20 | subclass is required to test the abstract class functionality. 21 | """ 22 | 23 | __author__ = "vasilios@google.com (Vasilios Hoffman)" 24 | 25 | import unittest 26 | 27 | from nss_cache.maps import automount 28 | from nss_cache.maps import passwd 29 | 30 | 31 | class TestAutomountMap(unittest.TestCase): 32 | """Tests for the AutomountMap class.""" 33 | 34 | def __init__(self, obj): 35 | """Set some default avalible data for testing.""" 36 | super(TestAutomountMap, self).__init__(obj) 37 | self._good_entry = automount.AutomountMapEntry() 38 | self._good_entry.key = "foo" 39 | self._good_entry.options = "-tcp" 40 | self._good_entry.location = "nfsserver:/mah/stuff" 41 | 42 | def testInit(self): 43 | """Construct an empty or seeded AutomountMap.""" 44 | self.assertEqual( 45 | automount.AutomountMap, 46 | type(automount.AutomountMap()), 47 | msg="failed to create an empty AutomountMap", 48 | ) 49 | amap = automount.AutomountMap([self._good_entry]) 50 | self.assertEqual( 51 | self._good_entry, 52 | amap.PopItem(), 53 | msg="failed to seed AutomountMap with list", 54 | ) 55 | self.assertRaises(TypeError, automount.AutomountMap, ["string"]) 56 | 57 | def testAdd(self): 58 | """Add throws an error for objects it can't verify.""" 59 | amap = automount.AutomountMap() 60 | entry = self._good_entry 61 | self.assertTrue(amap.Add(entry), msg="failed to append new entry.") 62 | 63 | self.assertEqual(1, len(amap), msg="unexpected size for Map.") 64 | 65 | ret_entry = amap.PopItem() 66 | self.assertEqual(ret_entry, entry, msg="failed to pop correct entry.") 67 | 68 | pentry = passwd.PasswdMapEntry() 69 | pentry.name = "foo" 70 | pentry.uid = 10 71 | pentry.gid = 10 72 | self.assertRaises(TypeError, amap.Add, pentry) 73 | 74 | 75 | class TestAutomountMapEntry(unittest.TestCase): 76 | """Tests for the AutomountMapEntry class.""" 77 | 78 | def testInit(self): 79 | """Construct an empty and seeded AutomountMapEntry.""" 80 | self.assertTrue( 81 | automount.AutomountMapEntry(), 82 | msg="Could not create empty AutomountMapEntry", 83 | ) 84 | seed = {"key": "foo", "location": "/dev/sda1"} 85 | entry = automount.AutomountMapEntry(seed) 86 | self.assertTrue(entry.Verify(), msg="Could not verify seeded AutomountMapEntry") 87 | self.assertEqual(entry.key, "foo", msg="Entry returned wrong value for name") 88 | self.assertEqual( 89 | entry.options, None, msg="Entry returned wrong value for options" 90 | ) 91 | self.assertEqual( 92 | entry.location, "/dev/sda1", msg="Entry returned wrong value for location" 93 | ) 94 | 95 | def testAttributes(self): 96 | """Test that we can get and set all expected attributes.""" 97 | entry = automount.AutomountMapEntry() 98 | entry.key = "foo" 99 | self.assertEqual(entry.key, "foo", msg="Could not set attribute: key") 100 | entry.options = "noatime" 101 | self.assertEqual( 102 | entry.options, "noatime", msg="Could not set attribute: options" 103 | ) 104 | entry.location = "/dev/ipod" 105 | self.assertEqual( 106 | entry.location, "/dev/ipod", msg="Could not set attribute: location" 107 | ) 108 | 109 | def testVerify(self): 110 | """Test that the object can verify it's attributes and itself.""" 111 | entry = automount.AutomountMapEntry() 112 | 113 | # Empty object should bomb 114 | self.assertFalse(entry.Verify()) 115 | 116 | def testKey(self): 117 | """Key() should return the value of the 'key' attribute.""" 118 | entry = automount.AutomountMapEntry() 119 | entry.key = "foo" 120 | self.assertEqual(entry.Key(), entry.key) 121 | 122 | 123 | if __name__ == "__main__": 124 | unittest.main() 125 | -------------------------------------------------------------------------------- /nss_cache/maps/group.py: -------------------------------------------------------------------------------- 1 | # Copyright 2007 Google Inc. 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | """An implementation of a group map for nsscache. 17 | 18 | GroupMap: An implementation of NSS group maps based on the Map 19 | class. 20 | 21 | GroupMapEntry: A group map entry based on the MapEntry class. 22 | """ 23 | 24 | __author__ = "vasilios@google.com (Vasilios Hoffman)" 25 | 26 | from nss_cache.maps import maps 27 | 28 | 29 | class GroupMap(maps.Map): 30 | """This class represents an NSS group map. 31 | 32 | Map data is stored as a list of MapEntry objects, see the abstract 33 | class Map. 34 | """ 35 | 36 | def __init__(self, iterable=None): 37 | """Construct a GroupMap object using optional iterable.""" 38 | super(GroupMap, self).__init__(iterable) 39 | 40 | def Add(self, entry): 41 | """Add a new object, verify it is a GroupMapEntry object.""" 42 | if not isinstance(entry, GroupMapEntry): 43 | raise TypeError 44 | return super(GroupMap, self).Add(entry) 45 | 46 | 47 | class GroupMapEntry(maps.MapEntry): 48 | """This class represents NSS group map entries.""" 49 | 50 | # Using slots saves us over 2x memory on large maps. 51 | __slots__ = ("name", "passwd", "gid", "members", "groupmembers") 52 | _KEY = "name" 53 | _ATTRS = ("name", "passwd", "gid", "members", "groupmembers") 54 | 55 | def __init__(self, data=None): 56 | """Construct a GroupMapEntry, setting reasonable defaults.""" 57 | self.name = None 58 | self.passwd = None 59 | self.gid = None 60 | self.members = None 61 | self.groupmembers = None 62 | 63 | super(GroupMapEntry, self).__init__(data) 64 | 65 | # Seed data with defaults if needed 66 | if self.passwd is None: 67 | self.passwd = "x" 68 | if self.members is None: 69 | self.members = [] 70 | if self.groupmembers is None: 71 | self.groupmembers = [] 72 | -------------------------------------------------------------------------------- /nss_cache/maps/group_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2007 Google Inc. 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | """Unit tests for group.py. 17 | 18 | We only test what is overridden in the group subclasses, most 19 | functionality is in base.py and tested in passwd_test.py since a 20 | subclass is required to test the abstract class functionality. 21 | """ 22 | 23 | __author__ = "vasilios@google.com (Vasilios Hoffman)" 24 | 25 | import unittest 26 | 27 | from nss_cache.maps import group 28 | from nss_cache.maps import passwd 29 | 30 | 31 | class TestGroupMap(unittest.TestCase): 32 | """Tests for the GroupMap class.""" 33 | 34 | def __init__(self, obj): 35 | """Set some default avalible data for testing.""" 36 | super(TestGroupMap, self).__init__(obj) 37 | self._good_entry = group.GroupMapEntry() 38 | self._good_entry.name = "foo" 39 | self._good_entry.passwd = "x" 40 | self._good_entry.gid = 10 41 | self._good_entry.members = ["foo", "bar"] 42 | 43 | def testInit(self): 44 | """Construct an empty or seeded GroupMap.""" 45 | self.assertEqual( 46 | group.GroupMap, 47 | type(group.GroupMap()), 48 | msg="failed to create an empty GroupMap", 49 | ) 50 | gmap = group.GroupMap([self._good_entry]) 51 | self.assertEqual( 52 | self._good_entry, gmap.PopItem(), msg="failed to seed GroupMap with list" 53 | ) 54 | self.assertRaises(TypeError, group.GroupMap, ["string"]) 55 | 56 | def testAdd(self): 57 | """Add throws an error for objects it can't verify.""" 58 | gmap = group.GroupMap() 59 | entry = self._good_entry 60 | self.assertTrue(gmap.Add(entry), msg="failed to append new entry.") 61 | 62 | self.assertEqual(1, len(gmap), msg="unexpected size for Map.") 63 | 64 | ret_entry = gmap.PopItem() 65 | self.assertEqual(ret_entry, entry, msg="failed to pop correct entry.") 66 | 67 | pentry = passwd.PasswdMapEntry() 68 | pentry.name = "foo" 69 | pentry.uid = 10 70 | pentry.gid = 10 71 | self.assertRaises(TypeError, gmap.Add, pentry) 72 | 73 | 74 | class TestGroupMapEntry(unittest.TestCase): 75 | """Tests for the GroupMapEntry class.""" 76 | 77 | def testInit(self): 78 | """Construct an empty and seeded GroupMapEntry.""" 79 | self.assertTrue( 80 | group.GroupMapEntry(), msg="Could not create empty GroupMapEntry" 81 | ) 82 | seed = {"name": "foo", "gid": 10} 83 | entry = group.GroupMapEntry(seed) 84 | self.assertTrue(entry.Verify(), msg="Could not verify seeded PasswdMapEntry") 85 | self.assertEqual(entry.name, "foo", msg="Entry returned wrong value for name") 86 | self.assertEqual(entry.passwd, "x", msg="Entry returned wrong value for passwd") 87 | self.assertEqual(entry.gid, 10, msg="Entry returned wrong value for gid") 88 | self.assertEqual( 89 | entry.members, [], msg="Entry returned wrong value for members" 90 | ) 91 | 92 | def testAttributes(self): 93 | """Test that we can get and set all expected attributes.""" 94 | entry = group.GroupMapEntry() 95 | entry.name = "foo" 96 | self.assertEqual(entry.name, "foo", msg="Could not set attribute: name") 97 | entry.passwd = "x" 98 | self.assertEqual(entry.passwd, "x", msg="Could not set attribute: passwd") 99 | entry.gid = 10 100 | self.assertEqual(entry.gid, 10, msg="Could not set attribute: gid") 101 | members = ["foo", "bar"] 102 | entry.members = members 103 | self.assertEqual(entry.members, members, msg="Could not set attribute: members") 104 | 105 | def testVerify(self): 106 | """Test that the object can verify it's attributes and itself.""" 107 | entry = group.GroupMapEntry() 108 | 109 | # Empty object should bomb 110 | self.assertFalse(entry.Verify()) 111 | 112 | def testKey(self): 113 | """Key() should return the value of the 'name' attribute.""" 114 | entry = group.GroupMapEntry() 115 | entry.name = "foo" 116 | self.assertEqual(entry.Key(), entry.name) 117 | 118 | 119 | if __name__ == "__main__": 120 | unittest.main() 121 | -------------------------------------------------------------------------------- /nss_cache/maps/maps_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2007 Google Inc. 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | """Unit test for base.py. 17 | 18 | Since these are abstract classes, the bulk of the functionality in 19 | base.py is specifically tested in passwd_test.py instead. 20 | """ 21 | 22 | __author__ = ( 23 | "jaq@google.com (Jamie Wilkinson)", 24 | "vasilios@google.com (Vasilios Hoffman)", 25 | ) 26 | 27 | import time 28 | import unittest 29 | 30 | from nss_cache.maps import maps 31 | 32 | 33 | class TestMap(unittest.TestCase): 34 | """Tests for the Map class.""" 35 | 36 | def testIsAbstract(self): 37 | """Creating a Map should raise a TypeError.""" 38 | self.assertRaises(TypeError, maps.Map) 39 | 40 | def testModifyTimestamp(self): 41 | class StubMap(maps.Map): 42 | pass 43 | 44 | foo = StubMap() 45 | now = int(time.time()) 46 | foo.SetModifyTimestamp(now) 47 | self.assertEqual(now, foo.GetModifyTimestamp()) 48 | self.assertRaises(TypeError, foo.SetModifyTimestamp, 1.1) 49 | foo.SetModifyTimestamp(None) 50 | self.assertEqual(None, foo.GetModifyTimestamp()) 51 | 52 | def testUpdateTimestamp(self): 53 | class StubMap(maps.Map): 54 | pass 55 | 56 | foo = StubMap() 57 | now = int(time.time()) 58 | foo.SetUpdateTimestamp(now) 59 | self.assertEqual(now, foo.GetUpdateTimestamp()) 60 | self.assertRaises(TypeError, foo.SetUpdateTimestamp, 1.1) 61 | foo.SetUpdateTimestamp(None) 62 | self.assertEqual(None, foo.GetUpdateTimestamp()) 63 | 64 | 65 | class TestMapEntry(unittest.TestCase): 66 | """Tests for the MapEntry class.""" 67 | 68 | def testIsAbstract(self): 69 | """Creating a MapEntry should raise a TypeError.""" 70 | self.assertRaises(TypeError, maps.MapEntry) 71 | 72 | 73 | if __name__ == "__main__": 74 | unittest.main() 75 | -------------------------------------------------------------------------------- /nss_cache/maps/netgroup.py: -------------------------------------------------------------------------------- 1 | # Copyright 2007 Google Inc. 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | """An implementation of a netgroup map for nsscache. 17 | 18 | NetgroupMap: An implementation of NSS netgroup maps based on the Map 19 | class. 20 | 21 | NetgroupMapEntry: A netgroup map entry based on the MapEntry class. 22 | 23 | Netgroup maps are somewhat different than the "typical" 24 | passwd/group/shadow maps. Instead of each entry having a fixed set of 25 | fields, each entry has an arbitrarily long list containing a arbitrary 26 | mix of other netgroup names or (host, user, domain) triples. 27 | 28 | Given the choice between more complex design, or just sticking a list 29 | of strings into each MapEntry class... the latter was chosen due to 30 | it's combination of simplicity and effectiveness. 31 | 32 | No provisioning is done in these classes to prevent infinite reference 33 | loops, e.g. a NetgroupMapEntry naming itself as a member, or 34 | unresolvable references. No dereferencing is ever done in these 35 | classes and datastores such as /etc/netgroup actually allow for those 36 | and similar cases. 37 | """ 38 | 39 | __author__ = "vasilios@google.com (Vasilios Hoffman)" 40 | 41 | from nss_cache.maps import maps 42 | 43 | 44 | class NetgroupMap(maps.Map): 45 | """This class represents an NSS netgroup map. 46 | 47 | Map data is stored as a list of MapEntry objects, see the abstract 48 | class Map. 49 | """ 50 | 51 | def __init__(self, iterable=None): 52 | """Construct a NetgroupMap object using optional iterable.""" 53 | super(NetgroupMap, self).__init__(iterable) 54 | 55 | def Add(self, entry): 56 | """Add a new object, verify it is a NetgroupMapEntry object.""" 57 | if not isinstance(entry, NetgroupMapEntry): 58 | raise TypeError 59 | return super(NetgroupMap, self).Add(entry) 60 | 61 | 62 | class NetgroupMapEntry(maps.MapEntry): 63 | """This class represents NSS netgroup map entries. 64 | 65 | The entries attribute is a list containing an arbitray mix of either 66 | strings which are netgroup names, or tuples mapping to (host, user, 67 | domain) as per the definition of netgroups. A None item in the 68 | tuple is the equivalent of a null pointer from getnetgrent(), 69 | specifically a wildcard. 70 | """ 71 | 72 | __slots__ = ("name", "entries") 73 | _KEY = "name" 74 | _ATTRS = ("name", "entries") 75 | 76 | def __init__(self, data=None): 77 | """Construct a NetgroupMapEntry.""" 78 | self.name = None 79 | self.entries = None 80 | 81 | super(NetgroupMapEntry, self).__init__(data) 82 | 83 | # Seed data with defaults if needed 84 | if self.entries is None: 85 | self.entries = "" 86 | -------------------------------------------------------------------------------- /nss_cache/maps/netgroup_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2007 Google Inc. 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | """Unit tests for netgroup.py. 17 | 18 | We only test what is overridden in the netgroup subclasses, most 19 | functionality is in base.py and tested in passwd_test.py since a 20 | subclass is required to test the abstract class functionality. 21 | """ 22 | 23 | __author__ = "vasilios@google.com (Vasilios Hoffman)" 24 | 25 | import unittest 26 | 27 | from nss_cache.maps import netgroup 28 | from nss_cache.maps import passwd 29 | 30 | 31 | class TestNetgroupMap(unittest.TestCase): 32 | """Tests for the NetgroupMap class.""" 33 | 34 | def __init__(self, obj): 35 | """Set some default avalible data for testing.""" 36 | super(TestNetgroupMap, self).__init__(obj) 37 | self._good_entry = netgroup.NetgroupMapEntry() 38 | self._good_entry.name = "foo" 39 | self._good_entry.entries = [("-", "bob", None), "othernetgroup"] 40 | 41 | def testInit(self): 42 | """Construct an empty or seeded NetgroupMap.""" 43 | self.assertEqual( 44 | netgroup.NetgroupMap, 45 | type(netgroup.NetgroupMap()), 46 | msg="failed to create an empty NetgroupMap", 47 | ) 48 | nmap = netgroup.NetgroupMap([self._good_entry]) 49 | self.assertEqual( 50 | self._good_entry, nmap.PopItem(), msg="failed to seed NetgroupMap with list" 51 | ) 52 | self.assertRaises(TypeError, netgroup.NetgroupMap, ["string"]) 53 | 54 | def testAdd(self): 55 | """Add throws an error for objects it can't verify.""" 56 | nmap = netgroup.NetgroupMap() 57 | entry = self._good_entry 58 | self.assertTrue(nmap.Add(entry), msg="failed to append new entry.") 59 | 60 | self.assertEqual(1, len(nmap), msg="unexpected size for Map.") 61 | 62 | ret_entry = nmap.PopItem() 63 | self.assertEqual(ret_entry, entry, msg="failed to pop correct entry.") 64 | 65 | pentry = passwd.PasswdMapEntry() 66 | pentry.name = "foo" 67 | pentry.uid = 10 68 | pentry.gid = 10 69 | self.assertRaises(TypeError, nmap.Add, pentry) 70 | 71 | 72 | class TestNetgroupMapEntry(unittest.TestCase): 73 | """Tests for the NetgroupMapEntry class.""" 74 | 75 | def testInit(self): 76 | """Construct an empty and seeded NetgroupMapEntry.""" 77 | self.assertTrue( 78 | netgroup.NetgroupMapEntry(), msg="Could not create empty NetgroupMapEntry" 79 | ) 80 | entries = ["bar", ("baz", "-", None)] 81 | seed = {"name": "foo", "entries": entries} 82 | entry = netgroup.NetgroupMapEntry(seed) 83 | self.assertTrue(entry.Verify(), msg="Could not verify seeded NetgroupMapEntry") 84 | self.assertEqual(entry.name, "foo", msg="Entry returned wrong value for name") 85 | self.assertEqual( 86 | entry.entries, entries, msg="Entry returned wrong value for entries" 87 | ) 88 | 89 | def testAttributes(self): 90 | """Test that we can get and set all expected attributes.""" 91 | entry = netgroup.NetgroupMapEntry() 92 | entry.name = "foo" 93 | self.assertEqual(entry.name, "foo", msg="Could not set attribute: name") 94 | entries = ["foo", "(-,bar,)"] 95 | entry.entries = entries 96 | self.assertEqual(entry.entries, entries, msg="Could not set attribute: entries") 97 | 98 | def testVerify(self): 99 | """Test that the object can verify it's attributes and itself.""" 100 | entry = netgroup.NetgroupMapEntry() 101 | 102 | # Empty object should bomb 103 | self.assertFalse(entry.Verify()) 104 | 105 | def testKey(self): 106 | """Key() should return the value of the 'name' attribute.""" 107 | entry = netgroup.NetgroupMapEntry() 108 | entry.name = "foo" 109 | self.assertEqual(entry.Key(), entry.name) 110 | 111 | 112 | if __name__ == "__main__": 113 | unittest.main() 114 | -------------------------------------------------------------------------------- /nss_cache/maps/passwd.py: -------------------------------------------------------------------------------- 1 | # Copyright 2007 Google Inc. 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | """An implementation of a passwd map for nsscache. 17 | 18 | PasswdMap: An implementation of NSS passwd maps based on the Map 19 | class. 20 | 21 | PasswdMapEntry: A passwd map entry based on the MapEntry class. 22 | """ 23 | 24 | __author__ = "vasilios@google.com (Vasilios Hoffman)" 25 | 26 | from nss_cache.maps import maps 27 | 28 | 29 | class PasswdMap(maps.Map): 30 | """This class represents an NSS passwd map. 31 | 32 | Map data is stored as a list of MapEntry objects, see the abstract 33 | class Map. 34 | """ 35 | 36 | def Add(self, entry): 37 | """Add a new object, verify it is a PasswdMapEntry instance. 38 | 39 | Args: 40 | entry: A PasswdMapEntry instance. 41 | 42 | Returns: 43 | True if added successfully, False otherwise. 44 | 45 | Raises: 46 | TypeError: The argument is of the wrong type. 47 | """ 48 | if not isinstance(entry, PasswdMapEntry): 49 | raise TypeError 50 | return super(PasswdMap, self).Add(entry) 51 | 52 | 53 | class PasswdMapEntry(maps.MapEntry): 54 | """This class represents NSS passwd map entries.""" 55 | 56 | # Using slots saves us over 2x memory on large maps. 57 | __slots__ = ("name", "uid", "gid", "passwd", "gecos", "dir", "shell") 58 | _KEY = "name" 59 | _ATTRS = ("name", "uid", "gid", "passwd", "gecos", "dir", "shell") 60 | 61 | def __init__(self, data=None): 62 | """Construct a PasswdMapEntry, setting reasonable defaults.""" 63 | self.name = None 64 | self.uid = None 65 | self.gid = None 66 | self.passwd = None 67 | self.gecos = None 68 | self.dir = None 69 | self.shell = None 70 | 71 | super(PasswdMapEntry, self).__init__(data) 72 | 73 | # Seed data with defaults if still empty 74 | if self.passwd is None: 75 | self.passwd = "x" 76 | if self.gecos is None: 77 | self.gecos = "" 78 | if self.dir is None: 79 | self.dir = "" 80 | if self.shell is None: 81 | self.shell = "" 82 | -------------------------------------------------------------------------------- /nss_cache/maps/shadow.py: -------------------------------------------------------------------------------- 1 | # Copyright 2007 Google Inc. 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | """An implementation of a shadow map for nsscache. 17 | 18 | ShadowMap: An implementation of NSS shadow maps based on the Map 19 | class. 20 | 21 | ShadowMapEntry: A shadow map entry based on the MapEntry class. 22 | """ 23 | 24 | __author__ = "vasilios@google.com (Vasilios Hoffman)" 25 | 26 | from nss_cache.maps import maps 27 | 28 | 29 | class ShadowMap(maps.Map): 30 | """This class represents an NSS shadow map. 31 | 32 | Map data is stored as a list of MapEntry objects, see the abstract 33 | class Map. 34 | """ 35 | 36 | def __init__(self, iterable=None): 37 | """Construct a ShadowMap object using optional iterable.""" 38 | super(ShadowMap, self).__init__(iterable) 39 | 40 | def Add(self, entry): 41 | """Add a new object, verify it is a ShadowMapEntry object.""" 42 | if not isinstance(entry, ShadowMapEntry): 43 | raise TypeError 44 | return super(ShadowMap, self).Add(entry) 45 | 46 | 47 | class ShadowMapEntry(maps.MapEntry): 48 | """This class represents NSS shadow map entries.""" 49 | 50 | __slots__ = ( 51 | "name", 52 | "passwd", 53 | "lstchg", 54 | "min", 55 | "max", 56 | "warn", 57 | "inact", 58 | "expire", 59 | "flag", 60 | ) 61 | _KEY = "name" 62 | _ATTRS = ( 63 | "name", 64 | "passwd", 65 | "lstchg", 66 | "min", 67 | "max", 68 | "warn", 69 | "inact", 70 | "expire", 71 | "flag", 72 | ) 73 | 74 | def __init__(self, data=None): 75 | """Construct a ShadowMapEntry, setting reasonable defaults.""" 76 | self.name = None 77 | self.passwd = None 78 | self.lstchg = None 79 | self.min = None 80 | self.max = None 81 | self.warn = None 82 | self.inact = None 83 | self.expire = None 84 | self.flag = None 85 | 86 | super(ShadowMapEntry, self).__init__(data) 87 | 88 | # Seed data with defaults if needed 89 | if self.passwd is None: 90 | self.passwd = "!!" 91 | -------------------------------------------------------------------------------- /nss_cache/maps/shadow_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2007 Google Inc. 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | """Unit tests for shadow.py. 17 | 18 | We only test what is overridden in the shadow subclasses, most 19 | functionality is in base.py and tested in passwd_test.py since a 20 | subclass is required to test the abstract class functionality. 21 | """ 22 | 23 | __author__ = "vasilios@google.com (Vasilios Hoffman)" 24 | 25 | import unittest 26 | 27 | from nss_cache.maps import passwd 28 | from nss_cache.maps import shadow 29 | 30 | 31 | class TestShadowMap(unittest.TestCase): 32 | """Tests for the ShadowMap class.""" 33 | 34 | def __init__(self, obj): 35 | """Set some default avalible data for testing.""" 36 | super(TestShadowMap, self).__init__(obj) 37 | self._good_entry = shadow.ShadowMapEntry() 38 | self._good_entry.name = "foo" 39 | self._good_entry.lstchg = None 40 | self._good_entry.min = None 41 | self._good_entry.max = None 42 | self._good_entry.warn = None 43 | self._good_entry.inact = None 44 | self._good_entry.expire = None 45 | self._good_entry.flag = None 46 | 47 | def testInit(self): 48 | """Construct an empty or seeded ShadowMap.""" 49 | self.assertEqual( 50 | shadow.ShadowMap, 51 | type(shadow.ShadowMap()), 52 | msg="failed to create emtpy ShadowMap", 53 | ) 54 | smap = shadow.ShadowMap([self._good_entry]) 55 | self.assertEqual( 56 | self._good_entry, smap.PopItem(), msg="failed to seed ShadowMap with list" 57 | ) 58 | self.assertRaises(TypeError, shadow.ShadowMap, ["string"]) 59 | 60 | def testAdd(self): 61 | """Add throws an error for objects it can't verify.""" 62 | smap = shadow.ShadowMap() 63 | entry = self._good_entry 64 | self.assertTrue(smap.Add(entry), msg="failed to append new entry.") 65 | 66 | self.assertEqual(1, len(smap), msg="unexpected size for Map.") 67 | 68 | ret_entry = smap.PopItem() 69 | self.assertEqual(ret_entry, entry, msg="failed to pop existing entry.") 70 | 71 | pentry = passwd.PasswdMapEntry() 72 | pentry.name = "foo" 73 | pentry.uid = 10 74 | pentry.gid = 10 75 | self.assertRaises(TypeError, smap.Add, pentry) 76 | 77 | 78 | class TestShadowMapEntry(unittest.TestCase): 79 | """Tests for the ShadowMapEntry class.""" 80 | 81 | def testInit(self): 82 | """Construct empty and seeded ShadowMapEntry.""" 83 | self.assertTrue( 84 | shadow.ShadowMapEntry(), msg="Could not create empty ShadowMapEntry" 85 | ) 86 | seed = {"name": "foo"} 87 | entry = shadow.ShadowMapEntry(seed) 88 | self.assertTrue(entry.Verify(), msg="Could not verify seeded ShadowMapEntry") 89 | self.assertEqual(entry.name, "foo", msg="Entry returned wrong value for name") 90 | self.assertEqual( 91 | entry.passwd, "!!", msg="Entry returned wrong value for passwd" 92 | ) 93 | self.assertEqual( 94 | entry.lstchg, None, msg="Entry returned wrong value for lstchg" 95 | ) 96 | self.assertEqual(entry.min, None, msg="Entry returned wrong value for min") 97 | self.assertEqual(entry.max, None, msg="Entry returned wrong value for max") 98 | self.assertEqual(entry.warn, None, msg="Entry returned wrong value for warn") 99 | self.assertEqual(entry.inact, None, msg="Entry returned wrong value for inact") 100 | self.assertEqual( 101 | entry.expire, None, msg="Entry returned wrong value for expire" 102 | ) 103 | self.assertEqual(entry.flag, None, msg="Entry returned wrong value for flag") 104 | 105 | def testAttributes(self): 106 | """Test that we can get and set all expected attributes.""" 107 | entry = shadow.ShadowMapEntry() 108 | entry.name = "foo" 109 | self.assertEqual(entry.name, "foo", msg="Could not set attribute: name") 110 | entry.passwd = "seekret" 111 | self.assertEqual(entry.passwd, "seekret", msg="Could not set attribute: passwd") 112 | entry.lstchg = 0 113 | self.assertEqual(entry.lstchg, 0, msg="Could not set attribute: lstchg") 114 | entry.min = 0 115 | self.assertEqual(entry.min, 0, msg="Could not set attribute: min") 116 | entry.max = 0 117 | self.assertEqual(entry.max, 0, msg="Could not set attribute: max") 118 | entry.warn = 0 119 | self.assertEqual(entry.warn, 0, msg="Could not set attribute: warn") 120 | entry.inact = 0 121 | self.assertEqual(entry.inact, 0, msg="Could not set attribute: inact") 122 | entry.expire = 0 123 | self.assertEqual(entry.expire, 0, msg="Could not set attribute: expire") 124 | entry.flag = 0 125 | self.assertEqual(entry.flag, 0, msg="Could not set attribute: flag") 126 | 127 | def testVerify(self): 128 | """Test that the object can verify it's attributes and itself.""" 129 | entry = shadow.ShadowMapEntry() 130 | 131 | # Emtpy object should bomb 132 | self.assertFalse(entry.Verify()) 133 | 134 | def testKey(self): 135 | """Key() should return the value of the 'name' attribute.""" 136 | entry = shadow.ShadowMapEntry() 137 | entry.name = "foo" 138 | self.assertEqual(entry.Key(), entry.name) 139 | 140 | 141 | if __name__ == "__main__": 142 | unittest.main() 143 | -------------------------------------------------------------------------------- /nss_cache/maps/sshkey.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Google Inc. 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | """An implementation of a sshkey map for nsscache. 17 | 18 | SshkeyMap: An implementation of NSS sshkey maps based on the Map 19 | class. 20 | 21 | SshkeyMapEntry: A sshkey map entry based on the MapEntry class. 22 | """ 23 | 24 | __author__ = "mimianddaniel@gmail.com" 25 | 26 | from nss_cache.maps import maps 27 | 28 | 29 | class SshkeyMap(maps.Map): 30 | """This class represents an NSS sshkey map. 31 | 32 | Map data is stored as a list of MapEntry objects, see the abstract 33 | class Map. 34 | """ 35 | 36 | def Add(self, entry): 37 | """Add a new object, verify it is a SshkeyMapEntry instance. 38 | 39 | Args: 40 | entry: A SshkeyMapEntry instance. 41 | 42 | Returns: 43 | True if added successfully, False otherwise. 44 | 45 | Raises: 46 | TypeError: The argument is of the wrong type. 47 | """ 48 | if not isinstance(entry, SshkeyMapEntry): 49 | raise TypeError 50 | return super(SshkeyMap, self).Add(entry) 51 | 52 | 53 | class SshkeyMapEntry(maps.MapEntry): 54 | """This class represents NSS sshkey map entries.""" 55 | 56 | # Using slots saves us over 2x memory on large maps. 57 | __slots__ = ("name", "sshkey") 58 | _KEY = "name" 59 | _ATTRS = ("name", "sshkey") 60 | 61 | def __init__(self, data=None): 62 | """Construct a SshkeyMapEntry, setting reasonable defaults.""" 63 | self.name = None 64 | self.sshkey = None 65 | 66 | super(SshkeyMapEntry, self).__init__(data) 67 | # Seed data with defaults if still empty 68 | if self.sshkey is None: 69 | self.sshkey = "" 70 | -------------------------------------------------------------------------------- /nss_cache/nss.py: -------------------------------------------------------------------------------- 1 | # Copyright 2007 Google Inc. 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | """NSS utility library.""" 17 | 18 | __author__ = "vasilios@google.com (Vasilios Hoffman)" 19 | 20 | import pwd 21 | import grp 22 | import logging 23 | import subprocess 24 | 25 | from nss_cache import config 26 | from nss_cache import error 27 | from nss_cache.maps import group 28 | from nss_cache.maps import passwd 29 | from nss_cache.maps import shadow 30 | 31 | # TODO(v): this should be a config option someday, but it's as standard 32 | # as libc so at the moment we'll leave it be for simplicity. 33 | GETENT = "/usr/bin/getent" 34 | 35 | 36 | def GetMap(map_name): 37 | """Retrieves a Map of type map_name via nss calls.""" 38 | 39 | if map_name == config.MAP_PASSWORD: 40 | return GetPasswdMap() 41 | elif map_name == config.MAP_GROUP: 42 | return GetGroupMap() 43 | elif map_name == config.MAP_SHADOW: 44 | return GetShadowMap() 45 | 46 | raise error.UnsupportedMap 47 | 48 | 49 | def GetPasswdMap(): 50 | """Returns a PasswdMap built from nss calls.""" 51 | passwd_map = passwd.PasswdMap() 52 | 53 | for nss_entry in pwd.getpwall(): 54 | map_entry = passwd.PasswdMapEntry() 55 | map_entry.name = nss_entry[0] 56 | map_entry.passwd = nss_entry[1] 57 | map_entry.uid = nss_entry[2] 58 | map_entry.gid = nss_entry[3] 59 | map_entry.gecos = nss_entry[4] 60 | map_entry.dir = nss_entry[5] 61 | map_entry.shell = nss_entry[6] 62 | passwd_map.Add(map_entry) 63 | 64 | return passwd_map 65 | 66 | 67 | def GetGroupMap(): 68 | """Returns a GroupMap built from nss calls.""" 69 | group_map = group.GroupMap() 70 | 71 | for nss_entry in grp.getgrall(): 72 | map_entry = group.GroupMapEntry() 73 | map_entry.name = nss_entry[0] 74 | map_entry.passwd = nss_entry[1] 75 | map_entry.gid = nss_entry[2] 76 | map_entry.members = nss_entry[3] 77 | if not map_entry.members: 78 | map_entry.members = [""] 79 | group_map.Add(map_entry) 80 | 81 | return group_map 82 | 83 | 84 | def GetShadowMap(): 85 | """Returns a ShadowMap built from nss calls.""" 86 | getent = _SpawnGetent(config.MAP_SHADOW) 87 | (getent_stdout, getent_stderr) = getent.communicate() 88 | 89 | # The following is going to be map-specific each time, so no point in 90 | # making more methods. 91 | shadow_map = shadow.ShadowMap() 92 | 93 | for line in getent_stdout.split(): 94 | line = line.decode("utf-8") 95 | nss_entry = line.strip().split(":") 96 | map_entry = shadow.ShadowMapEntry() 97 | map_entry.name = nss_entry[0] 98 | map_entry.passwd = nss_entry[1] 99 | if nss_entry[2] != "": 100 | map_entry.lstchg = int(nss_entry[2]) 101 | if nss_entry[3] != "": 102 | map_entry.min = int(nss_entry[3]) 103 | if nss_entry[4] != "": 104 | map_entry.max = int(nss_entry[4]) 105 | if nss_entry[5] != "": 106 | map_entry.warn = int(nss_entry[5]) 107 | if nss_entry[6] != "": 108 | map_entry.inact = int(nss_entry[6]) 109 | if nss_entry[7] != "": 110 | map_entry.expire = int(nss_entry[7]) 111 | if nss_entry[8] != "": 112 | map_entry.flag = int(nss_entry[8]) 113 | shadow_map.Add(map_entry) 114 | 115 | if getent_stderr: 116 | logging.debug("captured error %s", getent_stderr) 117 | 118 | retval = getent.returncode 119 | 120 | if retval != 0: 121 | logging.warning("%s returned error code: %d", GETENT, retval) 122 | 123 | return shadow_map 124 | 125 | 126 | def _SpawnGetent(map_name): 127 | """Run 'getent map' in a subprocess for reading NSS data.""" 128 | getent = subprocess.Popen( 129 | [GETENT, map_name], stdout=subprocess.PIPE, stderr=subprocess.PIPE 130 | ) 131 | 132 | return getent 133 | -------------------------------------------------------------------------------- /nss_cache/nss_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2007 Google Inc. 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | """Unit tests for nss_cache/nss.py.""" 17 | 18 | __author__ = "vasilios@google.com (Vasilios Hoffman)" 19 | 20 | import subprocess 21 | import unittest 22 | from unittest import mock 23 | 24 | from nss_cache import config 25 | from nss_cache import error 26 | from nss_cache import nss 27 | 28 | from nss_cache.maps import group 29 | from nss_cache.maps import passwd 30 | from nss_cache.maps import shadow 31 | 32 | 33 | class TestNSS(unittest.TestCase): 34 | """Tests for the NSS library.""" 35 | 36 | def testGetMap(self): 37 | """that GetMap is calling the right GetFooMap routines.""" 38 | 39 | # stub, retval, arg 40 | maps = [ 41 | ("GetPasswdMap", "TEST_PASSWORD", config.MAP_PASSWORD), 42 | ("GetGroupMap", "TEST_GROUP", config.MAP_GROUP), 43 | ("GetShadowMap", "TEST_SHADOW", config.MAP_SHADOW), 44 | ] 45 | 46 | for (fn, retval, arg) in maps: 47 | with mock.patch.object(nss, fn) as mock_map: 48 | mock_map.return_value = retval 49 | self.assertEqual(retval, nss.GetMap(arg)) 50 | mock_map.assert_called_once() 51 | 52 | def testGetMapException(self): 53 | """GetMap throws error.UnsupportedMap for unsupported maps.""" 54 | self.assertRaises(error.UnsupportedMap, nss.GetMap, "ohio") 55 | 56 | def testGetPasswdMap(self): 57 | """Verify we build a correct password map from nss calls.""" 58 | # mocks 59 | entry1 = passwd.PasswdMapEntry() 60 | entry1.name = "foo" 61 | entry1.uid = 10 62 | entry1.gid = 10 63 | entry1.gecos = "foo bar" 64 | entry1.dir = "/home/foo" 65 | entry1.shell = "/bin/shell" 66 | 67 | entry2 = passwd.PasswdMapEntry() 68 | entry2.name = "bar" 69 | entry2.uid = 20 70 | entry2.gid = 20 71 | entry2.gecos = "foo bar" 72 | entry2.dir = "/home/monkeyboy" 73 | entry2.shell = "/bin/shell" 74 | foo = ("foo", "x", 10, 10, "foo bar", "/home/foo", "/bin/shell") 75 | bar = ("bar", "x", 20, 20, "foo bar", "/home/monkeyboy", "/bin/shell") 76 | 77 | # stubs 78 | with mock.patch("pwd.getpwall") as mock_pwall: 79 | mock_pwall.return_value = [foo, bar] 80 | password_map = nss.GetPasswdMap() 81 | self.assertTrue(isinstance(password_map, passwd.PasswdMap)) 82 | self.assertEqual(len(password_map), 2) 83 | self.assertTrue(password_map.Exists(entry1)) 84 | self.assertTrue(password_map.Exists(entry2)) 85 | 86 | def testGetGroupMap(self): 87 | """Verify we build a correct group map from nss calls.""" 88 | 89 | # mocks 90 | entry1 = group.GroupMapEntry() 91 | entry1.name = "foo" 92 | entry1.passwd = "*" 93 | entry1.gid = 10 94 | entry1.members = [""] 95 | entry2 = group.GroupMapEntry() 96 | entry2.name = "bar" 97 | entry2.passwd = "*" 98 | entry2.gid = 20 99 | entry2.members = ["foo", "bar"] 100 | foo = ("foo", "*", 10, []) 101 | bar = ("bar", "*", 20, ["foo", "bar"]) 102 | 103 | # stubs 104 | with mock.patch("grp.getgrall") as mock_grpall: 105 | mock_grpall.return_value = [foo, bar] 106 | group_map = nss.GetGroupMap() 107 | self.assertTrue(isinstance(group_map, group.GroupMap)) 108 | self.assertEqual(len(group_map), 2) 109 | self.assertTrue(group_map.Exists(entry1)) 110 | self.assertTrue(group_map.Exists(entry2)) 111 | 112 | def testGetShadowMap(self): 113 | """Verify we build a correct shadow map from nss calls.""" 114 | line1 = b"foo:!!::::::::" 115 | line2 = b"bar:!!::::::::" 116 | 117 | entry1 = shadow.ShadowMapEntry() 118 | entry1.name = "foo" 119 | entry2 = shadow.ShadowMapEntry() 120 | entry2.name = "bar" 121 | 122 | with mock.patch.object(nss, "_SpawnGetent") as mock_getent: 123 | # stub 124 | mock_process = mock.create_autospec(subprocess.Popen) 125 | mock_getent.return_value = mock_process 126 | mock_process.communicate.return_value = [b"\n".join([line1, line2]), b""] 127 | mock_process.returncode = 0 128 | # test 129 | shadow_map = nss.GetShadowMap() 130 | self.assertTrue(isinstance(shadow_map, shadow.ShadowMap)) 131 | self.assertEqual(len(shadow_map), 2) 132 | self.assertTrue(shadow_map.Exists(entry1)) 133 | self.assertTrue(shadow_map.Exists(entry2)) 134 | 135 | 136 | if __name__ == "__main__": 137 | unittest.main() 138 | -------------------------------------------------------------------------------- /nss_cache/sources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/nsscache/66e3789910b2641e52707bba55d6e5d381069257/nss_cache/sources/__init__.py -------------------------------------------------------------------------------- /nss_cache/sources/consulsource.py: -------------------------------------------------------------------------------- 1 | """An implementation of a consul data source for nsscache.""" 2 | 3 | __author__ = "hexedpackets@gmail.com (William Huba)" 4 | 5 | import base64 6 | import collections 7 | import logging 8 | import json 9 | 10 | from nss_cache.maps import group 11 | from nss_cache.maps import passwd 12 | from nss_cache.maps import shadow 13 | from nss_cache.sources import httpsource 14 | 15 | 16 | def RegisterImplementation(registration_callback): 17 | registration_callback(ConsulFilesSource) 18 | 19 | 20 | class ConsulFilesSource(httpsource.HttpFilesSource): 21 | """Source for data fetched via Consul.""" 22 | 23 | # Consul defaults 24 | DATACENTER = "dc1" 25 | TOKEN = "" 26 | 27 | # for registration 28 | name = "consul" 29 | 30 | def _SetDefaults(self, configuration): 31 | """Set defaults if necessary.""" 32 | 33 | super(ConsulFilesSource, self)._SetDefaults(configuration) 34 | 35 | if "token" not in configuration: 36 | configuration["token"] = self.TOKEN 37 | if "datacenter" not in configuration: 38 | configuration["datacenter"] = self.DATACENTER 39 | 40 | for url in ["passwd_url", "group_url", "shadow_url"]: 41 | configuration[url] = "{}?recurse&token={}&dc={}".format( 42 | configuration[url], configuration["token"], configuration["datacenter"] 43 | ) 44 | 45 | def GetPasswdMap(self, since=None): 46 | """Return the passwd map from this source. 47 | 48 | Args: 49 | since: Get data only changed since this timestamp (inclusive) or None 50 | for all data. 51 | 52 | Returns: 53 | instance of passwd.PasswdMap 54 | """ 55 | return PasswdUpdateGetter().GetUpdates(self, self.conf["passwd_url"], since) 56 | 57 | def GetGroupMap(self, since=None): 58 | """Return the group map from this source. 59 | 60 | Args: 61 | since: Get data only changed since this timestamp (inclusive) or None 62 | for all data. 63 | 64 | Returns: 65 | instance of group.GroupMap 66 | """ 67 | return GroupUpdateGetter().GetUpdates(self, self.conf["group_url"], since) 68 | 69 | def GetShadowMap(self, since=None): 70 | """Return the shadow map from this source. 71 | 72 | Args: 73 | since: Get data only changed since this timestamp (inclusive) or None 74 | for all data. 75 | 76 | Returns: 77 | instance of shadow.ShadowMap 78 | """ 79 | return ShadowUpdateGetter().GetUpdates(self, self.conf["shadow_url"], since) 80 | 81 | 82 | class PasswdUpdateGetter(httpsource.UpdateGetter): 83 | """Get passwd updates.""" 84 | 85 | def GetParser(self): 86 | """Returns a MapParser to parse FilesPasswd cache.""" 87 | return ConsulPasswdMapParser() 88 | 89 | def CreateMap(self): 90 | """Returns a new PasswdMap instance to have PasswdMapEntries added to 91 | it.""" 92 | return passwd.PasswdMap() 93 | 94 | 95 | class GroupUpdateGetter(httpsource.UpdateGetter): 96 | """Get group updates.""" 97 | 98 | def GetParser(self): 99 | """Returns a MapParser to parse FilesGroup cache.""" 100 | return ConsulGroupMapParser() 101 | 102 | def CreateMap(self): 103 | """Returns a new GroupMap instance to have GroupMapEntries added to 104 | it.""" 105 | return group.GroupMap() 106 | 107 | 108 | class ShadowUpdateGetter(httpsource.UpdateGetter): 109 | """Get shadow updates.""" 110 | 111 | def GetParser(self): 112 | """Returns a MapParser to parse FilesShadow cache.""" 113 | return ConsulShadowMapParser() 114 | 115 | def CreateMap(self): 116 | """Returns a new ShadowMap instance to have ShadowMapEntries added to 117 | it.""" 118 | return shadow.ShadowMap() 119 | 120 | 121 | class ConsulMapParser(object): 122 | """A base class for parsing nss_files module cache.""" 123 | 124 | def __init__(self): 125 | self.log = logging.getLogger(__name__) 126 | 127 | def GetMap(self, cache_info, data): 128 | """Returns a map from a cache. 129 | 130 | Args: 131 | cache_info: file like object containing the cache. 132 | data: a Map to populate. 133 | Returns: 134 | A child of Map containing the cache data. 135 | """ 136 | 137 | entries = collections.defaultdict(dict) 138 | for line in json.loads(cache_info.read()): 139 | key = line.get("Key", "").split("/") 140 | value = line.get("Value", "") 141 | if not value or not key: 142 | continue 143 | value = base64.b64decode(value) 144 | name = str(key[-2]) 145 | entry_piece = key[-1] 146 | entries[name][entry_piece] = value 147 | 148 | for name, entry in list(entries.items()): 149 | map_entry = self._ReadEntry(name, entry) 150 | if map_entry is None: 151 | self.log.warning( 152 | "Could not create entry from line %r in cache, skipping", entry 153 | ) 154 | continue 155 | if not data.Add(map_entry): 156 | self.log.warning( 157 | "Could not add entry %r read from line %r in cache", 158 | map_entry, 159 | entry, 160 | ) 161 | return data 162 | 163 | 164 | class ConsulPasswdMapParser(ConsulMapParser): 165 | """Class for parsing nss_files module passwd cache.""" 166 | 167 | def _ReadEntry(self, name, entry): 168 | """Return a PasswdMapEntry from a record in the target cache.""" 169 | 170 | map_entry = passwd.PasswdMapEntry() 171 | # maps expect strict typing, so convert to int as appropriate. 172 | map_entry.name = name 173 | map_entry.passwd = entry.get("passwd", "x") 174 | 175 | try: 176 | map_entry.uid = int(entry["uid"]) 177 | map_entry.gid = int(entry["gid"]) 178 | except (ValueError, KeyError): 179 | return None 180 | 181 | map_entry.gecos = entry.get("comment", "") 182 | map_entry.dir = entry.get("home", "/home/{}".format(name)) 183 | map_entry.shell = entry.get("shell", "/bin/bash") 184 | 185 | return map_entry 186 | 187 | 188 | class ConsulGroupMapParser(ConsulMapParser): 189 | """Class for parsing a nss_files module group cache.""" 190 | 191 | def _ReadEntry(self, name, entry): 192 | """Return a GroupMapEntry from a record in the target cache.""" 193 | 194 | map_entry = group.GroupMapEntry() 195 | # map entries expect strict typing, so convert as appropriate 196 | map_entry.name = name 197 | map_entry.passwd = entry.get("passwd", "x") 198 | 199 | try: 200 | map_entry.gid = int(entry["gid"]) 201 | except (ValueError, KeyError): 202 | return None 203 | 204 | try: 205 | s = entry.get("members", "").decode("utf-8") 206 | members = s.split("\n") 207 | except AttributeError: 208 | members = entry.get("members", "").split("\n") 209 | except (ValueError, TypeError): 210 | members = [""] 211 | map_entry.members = members 212 | return map_entry 213 | 214 | 215 | class ConsulShadowMapParser(ConsulMapParser): 216 | """Class for parsing nss_files module shadow cache.""" 217 | 218 | def _ReadEntry(self, name, entry): 219 | """Return a ShadowMapEntry from a record in the target cache.""" 220 | 221 | map_entry = shadow.ShadowMapEntry() 222 | # maps expect strict typing, so convert to int as appropriate. 223 | map_entry.name = name 224 | map_entry.passwd = entry.get("passwd", "*") 225 | if isinstance(map_entry.passwd, bytes): 226 | map_entry.passwd = map_entry.passwd.decode("ascii") 227 | 228 | for attr in ["lstchg", "min", "max", "warn", "inact", "expire"]: 229 | try: 230 | setattr(map_entry, attr, int(entry[attr])) 231 | except (ValueError, KeyError): 232 | continue 233 | 234 | return map_entry 235 | -------------------------------------------------------------------------------- /nss_cache/sources/consulsource_test.py: -------------------------------------------------------------------------------- 1 | """An implementation of a mock consul data source for nsscache.""" 2 | 3 | __author__ = "hexedpackets@gmail.com (William Huba)" 4 | 5 | import unittest 6 | from io import StringIO 7 | 8 | from nss_cache.maps import group 9 | from nss_cache.maps import passwd 10 | from nss_cache.maps import shadow 11 | from nss_cache.sources import consulsource 12 | 13 | 14 | class TestConsulSource(unittest.TestCase): 15 | def setUp(self): 16 | """Initialize a basic config dict.""" 17 | super(TestConsulSource, self).setUp() 18 | self.config = { 19 | "passwd_url": "PASSWD_URL", 20 | "group_url": "GROUP_URL", 21 | "datacenter": "TEST_DATACENTER", 22 | "token": "TEST_TOKEN", 23 | } 24 | 25 | def testDefaultConfiguration(self): 26 | source = consulsource.ConsulFilesSource({}) 27 | self.assertEqual( 28 | source.conf["datacenter"], consulsource.ConsulFilesSource.DATACENTER 29 | ) 30 | self.assertEqual(source.conf["token"], consulsource.ConsulFilesSource.TOKEN) 31 | 32 | def testOverrideDefaultConfiguration(self): 33 | source = consulsource.ConsulFilesSource(self.config) 34 | self.assertEqual(source.conf["datacenter"], "TEST_DATACENTER") 35 | self.assertEqual(source.conf["token"], "TEST_TOKEN") 36 | self.assertEqual( 37 | source.conf["passwd_url"], 38 | "PASSWD_URL?recurse&token=TEST_TOKEN&dc=TEST_DATACENTER", 39 | ) 40 | self.assertEqual( 41 | source.conf["group_url"], 42 | "GROUP_URL?recurse&token=TEST_TOKEN&dc=TEST_DATACENTER", 43 | ) 44 | 45 | 46 | class TestPasswdMapParser(unittest.TestCase): 47 | def setUp(self): 48 | """Set some default avalible data for testing.""" 49 | self.good_entry = passwd.PasswdMapEntry() 50 | self.good_entry.name = "foo" 51 | self.good_entry.passwd = "x" 52 | self.good_entry.uid = 10 53 | self.good_entry.gid = 10 54 | self.good_entry.gecos = b"How Now Brown Cow" 55 | self.good_entry.dir = b"/home/foo" 56 | self.good_entry.shell = b"/bin/bash" 57 | self.parser = consulsource.ConsulPasswdMapParser() 58 | 59 | def testGetMap(self): 60 | passwd_map = passwd.PasswdMap() 61 | cache_info = StringIO( 62 | """[ 63 | {"Key": "org/users/foo/uid", "Value": "MTA="}, 64 | {"Key": "org/users/foo/gid", "Value": "MTA="}, 65 | {"Key": "org/users/foo/home", "Value": "L2hvbWUvZm9v"}, 66 | {"Key": "org/users/foo/shell", "Value": "L2Jpbi9iYXNo"}, 67 | {"Key": "org/users/foo/comment", "Value": "SG93IE5vdyBCcm93biBDb3c="}, 68 | {"Key": "org/users/foo/subkey/irrelevant_key", "Value": "YmFjb24="} 69 | ]""" 70 | ) 71 | self.parser.GetMap(cache_info, passwd_map) 72 | self.assertEqual(self.good_entry, passwd_map.PopItem()) 73 | 74 | def testReadEntry(self): 75 | data = { 76 | "uid": "10", 77 | "gid": "10", 78 | "comment": b"How Now Brown Cow", 79 | "shell": b"/bin/bash", 80 | "home": b"/home/foo", 81 | "passwd": "x", 82 | } 83 | entry = self.parser._ReadEntry("foo", data) 84 | self.assertEqual(self.good_entry, entry) 85 | 86 | def testDefaultEntryValues(self): 87 | data = {"uid": "10", "gid": "10"} 88 | entry = self.parser._ReadEntry("foo", data) 89 | self.assertEqual(entry.shell, "/bin/bash") 90 | self.assertEqual(entry.dir, "/home/foo") 91 | self.assertEqual(entry.gecos, "") 92 | self.assertEqual(entry.passwd, "x") 93 | 94 | def testInvalidEntry(self): 95 | data = {"irrelevant_key": "bacon"} 96 | entry = self.parser._ReadEntry("foo", data) 97 | self.assertEqual(entry, None) 98 | 99 | 100 | class TestConsulGroupMapParser(unittest.TestCase): 101 | def setUp(self): 102 | self.good_entry = group.GroupMapEntry() 103 | self.good_entry.name = "foo" 104 | self.good_entry.passwd = "x" 105 | self.good_entry.gid = 10 106 | self.good_entry.members = ["foo", "bar"] 107 | self.parser = consulsource.ConsulGroupMapParser() 108 | 109 | def testGetMap(self): 110 | group_map = group.GroupMap() 111 | cache_info = StringIO( 112 | """[ 113 | {"Key": "org/groups/foo/gid", "Value": "MTA="}, 114 | {"Key": "org/groups/foo/members", "Value": "Zm9vCmJhcg=="}, 115 | {"Key": "org/groups/foo/subkey/irrelevant_key", "Value": "YmFjb24="} 116 | ]""" 117 | ) 118 | self.parser.GetMap(cache_info, group_map) 119 | self.assertEqual(self.good_entry, group_map.PopItem()) 120 | 121 | def testReadEntry(self): 122 | data = {"passwd": "x", "gid": "10", "members": "foo\nbar"} 123 | entry = self.parser._ReadEntry("foo", data) 124 | self.assertEqual(self.good_entry, entry) 125 | 126 | def testDefaultPasswd(self): 127 | data = {"gid": "10", "members": "foo\nbar"} 128 | entry = self.parser._ReadEntry("foo", data) 129 | self.assertEqual(self.good_entry, entry) 130 | 131 | def testNoMembers(self): 132 | data = {"gid": "10", "members": ""} 133 | entry = self.parser._ReadEntry("foo", data) 134 | self.assertEqual(entry.members, [""]) 135 | 136 | def testInvalidEntry(self): 137 | data = {"irrelevant_key": "bacon"} 138 | entry = self.parser._ReadEntry("foo", data) 139 | self.assertEqual(entry, None) 140 | 141 | 142 | class TestConsulShadowMapParser(unittest.TestCase): 143 | def setUp(self): 144 | self.good_entry = shadow.ShadowMapEntry() 145 | self.good_entry.name = "foo" 146 | self.good_entry.passwd = "*" 147 | self.good_entry.lstchg = 17246 148 | self.good_entry.min = 0 149 | self.good_entry.max = 99999 150 | self.good_entry.warn = 7 151 | self.parser = consulsource.ConsulShadowMapParser() 152 | 153 | def testGetMap(self): 154 | shadow_map = shadow.ShadowMap() 155 | cache_info = StringIO( 156 | """[ 157 | {"Key": "org/groups/foo/passwd", "Value": "Kg=="}, 158 | {"Key": "org/groups/foo/lstchg", "Value": "MTcyNDY="}, 159 | {"Key": "org/groups/foo/min", "Value": "MA=="}, 160 | {"Key": "org/groups/foo/max", "Value": "OTk5OTk="}, 161 | {"Key": "org/groups/foo/warn", "Value": "Nw=="} 162 | ]""" 163 | ) 164 | self.parser.GetMap(cache_info, shadow_map) 165 | self.assertEqual(self.good_entry, shadow_map.PopItem()) 166 | 167 | def testReadEntry(self): 168 | data = {"passwd": "*", "lstchg": 17246, "min": 0, "max": 99999, "warn": 7} 169 | entry = self.parser._ReadEntry("foo", data) 170 | self.assertEqual(self.good_entry, entry) 171 | 172 | def testDefaultPasswd(self): 173 | data = {"lstchg": 17246, "min": 0, "max": 99999, "warn": 7} 174 | entry = self.parser._ReadEntry("foo", data) 175 | self.assertEqual(self.good_entry, entry) 176 | 177 | 178 | if __name__ == "__main__": 179 | unittest.main() 180 | -------------------------------------------------------------------------------- /nss_cache/sources/gcssource.py: -------------------------------------------------------------------------------- 1 | """An implementation of a GCS data source for nsscache.""" 2 | 3 | import logging 4 | import warnings 5 | 6 | from google.cloud import storage 7 | 8 | from nss_cache import error 9 | from nss_cache.maps import group 10 | from nss_cache.maps import passwd 11 | from nss_cache.maps import shadow 12 | from nss_cache.sources import source 13 | from nss_cache.util import file_formats 14 | from nss_cache.util import timestamps 15 | 16 | warnings.filterwarnings( 17 | "ignore", "Your application has authenticated using end user credentials" 18 | ) 19 | 20 | 21 | def RegisterImplementation(registration_callback): 22 | registration_callback(GcsFilesSource) 23 | 24 | 25 | class GcsFilesSource(source.Source): 26 | """Source for data fetched from GCS.""" 27 | 28 | # GCS Defaults 29 | BUCKET = "" 30 | PASSWD_OBJECT = "" 31 | GROUP_OBJECT = "" 32 | SHADOW_OBJECT = "" 33 | 34 | # for registration 35 | name = "gcs" 36 | 37 | def __init__(self, conf): 38 | """Initialize the GcsFilesSource object. 39 | 40 | Args: 41 | conf: A dictionary of key/value pairs. 42 | 43 | Raises: 44 | RuntimeError: object wasn't initialized with a dict. 45 | """ 46 | super(GcsFilesSource, self).__init__(conf) 47 | self._SetDefaults(conf) 48 | self._gcs_client = None 49 | 50 | def _GetClient(self): 51 | if self._gcs_client is None: 52 | self._gcs_client = storage.Client() 53 | return self._gcs_client 54 | 55 | def _SetDefaults(self, configuration): 56 | """Set defaults if necessary.""" 57 | 58 | if "bucket" not in configuration: 59 | configuration["bucket"] = self.BUCKET 60 | if "passwd_object" not in configuration: 61 | configuration["passwd_object"] = self.PASSWD_OBJECT 62 | if "group_object" not in configuration: 63 | configuration["group_object"] = self.GROUP_OBJECT 64 | if "shadow_object" not in configuration: 65 | configuration["shadow_object"] = self.SHADOW_OBJECT 66 | 67 | def GetPasswdMap(self, since=None): 68 | """Return the passwd map from this source. 69 | 70 | Args: 71 | since: Get data only changed since this timestamp (inclusive) or None 72 | for all data. 73 | 74 | Returns: 75 | instance of passwd.PasswdMap 76 | """ 77 | return PasswdUpdateGetter().GetUpdates( 78 | self._GetClient(), self.conf["bucket"], self.conf["passwd_object"], since 79 | ) 80 | 81 | def GetGroupMap(self, since=None): 82 | """Return the group map from this source. 83 | 84 | Args: 85 | since: Get data only changed since this timestamp (inclusive) or None 86 | for all data. 87 | 88 | Returns: 89 | instance of group.GroupMap 90 | """ 91 | return GroupUpdateGetter().GetUpdates( 92 | self._GetClient(), self.conf["bucket"], self.conf["group_object"], since 93 | ) 94 | 95 | def GetShadowMap(self, since=None): 96 | """Return the shadow map from this source. 97 | 98 | Args: 99 | since: Get data only changed since this timestamp (inclusive) or None 100 | for all data. 101 | 102 | Returns: 103 | instance of shadow.ShadowMap 104 | """ 105 | return ShadowUpdateGetter().GetUpdates( 106 | self._GetClient(), self.conf["bucket"], self.conf["shadow_object"], since 107 | ) 108 | 109 | 110 | class GcsUpdateGetter(object): 111 | """Base class that gets updates from GCS.""" 112 | 113 | def __init__(self): 114 | self.log = logging.getLogger(__name__) 115 | 116 | def GetUpdates(self, gcs_client, bucket_name, obj, since): 117 | """Gets updates from a source. 118 | 119 | Args: 120 | gcs_client: initialized gcs client 121 | bucket_name: gcs bucket name 122 | obj: object with the data 123 | since: a timestamp representing the last change (None to force-get) 124 | 125 | Returns: 126 | A tuple containing the map of updates and a maximum timestamp 127 | """ 128 | bucket = gcs_client.bucket(bucket_name) 129 | blob = bucket.get_blob(obj) 130 | # get_blob captures NotFound error and returns None: 131 | if blob is None: 132 | self.log.error("GCS object gs://%s/%s not found", bucket_name, obj) 133 | raise error.SourceUnavailable("unable to download object from GCS.") 134 | # GCS doesn't return HTTP 304 like HTTP or S3 sources, 135 | # so return if updated timestamp is before 'since': 136 | if since and timestamps.FromDateTimeToTimestamp(blob.updated) < since: 137 | return [] 138 | 139 | data_map = self.GetMap(cache_info=blob.open()) 140 | data_map.SetModifyTimestamp(timestamps.FromDateTimeToTimestamp(blob.updated)) 141 | return data_map 142 | 143 | def GetParser(self): 144 | """Return the approriate parser. 145 | 146 | Must be implemented by child class. 147 | """ 148 | raise NotImplementedError 149 | 150 | def GetMap(self, cache_info): 151 | """Creates a Map from the cache_info data. 152 | 153 | Args: 154 | cache_info: file-like object containing the data to parse 155 | 156 | Returns: 157 | A child of Map containing the cache data. 158 | """ 159 | return self.GetParser().GetMap(cache_info, self.CreateMap()) 160 | 161 | 162 | class PasswdUpdateGetter(GcsUpdateGetter): 163 | """Get passwd updates.""" 164 | 165 | def GetParser(self): 166 | """Returns a MapParser to parse FilesPasswd cache.""" 167 | return file_formats.FilesPasswdMapParser() 168 | 169 | def CreateMap(self): 170 | """Returns a new PasswdMap instance to have PasswdMapEntries added to it.""" 171 | return passwd.PasswdMap() 172 | 173 | 174 | class GroupUpdateGetter(GcsUpdateGetter): 175 | """Get group updates.""" 176 | 177 | def GetParser(self): 178 | """Returns a MapParser to parse FilesGroup cache.""" 179 | return file_formats.FilesGroupMapParser() 180 | 181 | def CreateMap(self): 182 | """Returns a new GroupMap instance to have GroupMapEntries added to it.""" 183 | return group.GroupMap() 184 | 185 | 186 | class ShadowUpdateGetter(GcsUpdateGetter): 187 | """Get shadow updates.""" 188 | 189 | def GetParser(self): 190 | """Returns a MapParser to parse FilesShadow cache.""" 191 | return file_formats.FilesShadowMapParser() 192 | 193 | def CreateMap(self): 194 | """Returns a new ShadowMap instance to have ShadowMapEntries added to it.""" 195 | return shadow.ShadowMap() 196 | -------------------------------------------------------------------------------- /nss_cache/sources/gcssource_test.py: -------------------------------------------------------------------------------- 1 | """An implementation of a mock GCS data source for nsscache.""" 2 | 3 | import datetime 4 | import io 5 | import unittest 6 | from unittest import mock 7 | 8 | from nss_cache.maps import group 9 | from nss_cache.maps import passwd 10 | from nss_cache.maps import shadow 11 | from nss_cache.util import file_formats 12 | from nss_cache.util import timestamps 13 | 14 | try: 15 | from nss_cache.sources import gcssource 16 | except Exception as e: 17 | raise unittest.SkipTest("`gcssource` unabled to be imported: {}".format(e)) 18 | 19 | 20 | class TestGcsSource(unittest.TestCase): 21 | def setUp(self): 22 | super(TestGcsSource, self).setUp() 23 | self.config = { 24 | "passwd_object": "PASSWD_OBJ", 25 | "group_object": "GROUP_OBJ", 26 | "bucket": "TEST_BUCKET", 27 | } 28 | 29 | def testDefaultConfiguration(self): 30 | source = gcssource.GcsFilesSource({}) 31 | self.assertEqual(source.conf["bucket"], gcssource.GcsFilesSource.BUCKET) 32 | self.assertEqual( 33 | source.conf["passwd_object"], gcssource.GcsFilesSource.PASSWD_OBJECT 34 | ) 35 | 36 | def testOverrideDefaultConfiguration(self): 37 | source = gcssource.GcsFilesSource(self.config) 38 | self.assertEqual(source.conf["bucket"], "TEST_BUCKET") 39 | self.assertEqual(source.conf["passwd_object"], "PASSWD_OBJ") 40 | self.assertEqual(source.conf["group_object"], "GROUP_OBJ") 41 | 42 | 43 | class TestPasswdUpdateGetter(unittest.TestCase): 44 | def setUp(self): 45 | super(TestPasswdUpdateGetter, self).setUp() 46 | self.updater = gcssource.PasswdUpdateGetter() 47 | 48 | def testGetParser(self): 49 | self.assertIsInstance( 50 | self.updater.GetParser(), file_formats.FilesPasswdMapParser 51 | ) 52 | 53 | def testCreateMap(self): 54 | self.assertIsInstance(self.updater.CreateMap(), passwd.PasswdMap) 55 | 56 | 57 | class TestShadowUpdateGetter(unittest.TestCase): 58 | def setUp(self): 59 | super(TestShadowUpdateGetter, self).setUp() 60 | self.updater = gcssource.ShadowUpdateGetter() 61 | 62 | def testGetParser(self): 63 | self.assertIsInstance( 64 | self.updater.GetParser(), file_formats.FilesShadowMapParser 65 | ) 66 | 67 | def testCreateMap(self): 68 | self.assertIsInstance(self.updater.CreateMap(), shadow.ShadowMap) 69 | 70 | def testShadowGetUpdatesWithContent(self): 71 | mock_client = mock.Mock() 72 | mock_bucket = mock_client.bucket.return_value 73 | mock_blob = mock_bucket.get_blob.return_value 74 | mock_blob.open.return_value = io.StringIO( 75 | """usera:x::::::: 76 | userb:x::::::: 77 | """ 78 | ) 79 | mock_blob.updated = datetime.datetime.now() 80 | 81 | result = self.updater.GetUpdates(mock_client, "test-bucket", "passwd", None) 82 | 83 | self.assertEqual(len(result), 2) 84 | mock_bucket.get_blob.assert_called_with("passwd") 85 | mock_client.bucket.assert_called_with("test-bucket") 86 | 87 | def testShadowGetUpdatesSinceAfterUpdatedTime(self): 88 | mock_client = mock.Mock() 89 | mock_bucket = mock_client.bucket.return_value 90 | mock_blob = mock_bucket.get_blob.return_value 91 | now = datetime.datetime.now() 92 | mock_blob.updated = now 93 | 94 | result = self.updater.GetUpdates( 95 | mock_client, 96 | "test-bucket", 97 | "passwd", 98 | timestamps.FromDateTimeToTimestamp(now + datetime.timedelta(days=1)), 99 | ) 100 | 101 | self.assertEqual(len(result), 0) 102 | mock_bucket.get_blob.assert_called_with("passwd") 103 | mock_client.bucket.assert_called_with("test-bucket") 104 | 105 | 106 | class TestGroupUpdateGetter(unittest.TestCase): 107 | def setUp(self): 108 | super(TestGroupUpdateGetter, self).setUp() 109 | self.updater = gcssource.GroupUpdateGetter() 110 | 111 | def testGetParser(self): 112 | self.assertIsInstance( 113 | self.updater.GetParser(), file_formats.FilesGroupMapParser 114 | ) 115 | 116 | def testCreateMap(self): 117 | self.assertIsInstance(self.updater.CreateMap(), group.GroupMap) 118 | 119 | 120 | if __name__ == "__main__": 121 | unittest.main() 122 | -------------------------------------------------------------------------------- /nss_cache/sources/s3source_test.py: -------------------------------------------------------------------------------- 1 | """An implementation of a mock S3 data source for nsscache.""" 2 | 3 | __author__ = "alexey.pikin@gmail.com" 4 | 5 | import unittest 6 | from io import StringIO 7 | 8 | from nss_cache.maps import group 9 | from nss_cache.maps import passwd 10 | from nss_cache.maps import shadow 11 | 12 | try: 13 | from nss_cache.sources import s3source 14 | except Exception as e: 15 | raise unittest.SkipTest("s3source unable to be imported: {}".format(e)) 16 | 17 | 18 | class TestS3Source(unittest.TestCase): 19 | def setUp(self): 20 | """Initialize a basic config dict.""" 21 | super(TestS3Source, self).setUp() 22 | self.config = { 23 | "passwd_object": "PASSWD_OBJ", 24 | "group_object": "GROUP_OBJ", 25 | "bucket": "TEST_BUCKET", 26 | } 27 | 28 | def testDefaultConfiguration(self): 29 | source = s3source.S3FilesSource({}) 30 | self.assertEqual(source.conf["bucket"], s3source.S3FilesSource.BUCKET) 31 | self.assertEqual( 32 | source.conf["passwd_object"], s3source.S3FilesSource.PASSWD_OBJECT 33 | ) 34 | 35 | def testOverrideDefaultConfiguration(self): 36 | source = s3source.S3FilesSource(self.config) 37 | self.assertEqual(source.conf["bucket"], "TEST_BUCKET") 38 | self.assertEqual(source.conf["passwd_object"], "PASSWD_OBJ") 39 | self.assertEqual(source.conf["group_object"], "GROUP_OBJ") 40 | 41 | 42 | class TestPasswdMapParser(unittest.TestCase): 43 | def setUp(self): 44 | """Set some default avalible data for testing.""" 45 | self.good_entry = passwd.PasswdMapEntry() 46 | self.good_entry.name = "foo" 47 | self.good_entry.passwd = "x" 48 | self.good_entry.uid = 10 49 | self.good_entry.gid = 10 50 | self.good_entry.gecos = "How Now Brown Cow" 51 | self.good_entry.dir = "/home/foo" 52 | self.good_entry.shell = "/bin/bash" 53 | self.parser = s3source.S3PasswdMapParser() 54 | 55 | def testGetMap(self): 56 | passwd_map = passwd.PasswdMap() 57 | cache_info = StringIO( 58 | """[ 59 | { "Key": "foo", 60 | "Value": { 61 | "uid": 10, "gid": 10, "home": "/home/foo", 62 | "shell": "/bin/bash", "comment": "How Now Brown Cow", 63 | "irrelevant_key":"bacon" 64 | } 65 | } 66 | ]""" 67 | ) 68 | self.parser.GetMap(cache_info, passwd_map) 69 | self.assertEqual(self.good_entry, passwd_map.PopItem()) 70 | 71 | def testReadEntry(self): 72 | data = { 73 | "uid": "10", 74 | "gid": "10", 75 | "comment": "How Now Brown Cow", 76 | "shell": "/bin/bash", 77 | "home": "/home/foo", 78 | "passwd": "x", 79 | } 80 | entry = self.parser._ReadEntry("foo", data) 81 | self.assertEqual(self.good_entry, entry) 82 | 83 | def testDefaultEntryValues(self): 84 | data = {"uid": "10", "gid": "10"} 85 | entry = self.parser._ReadEntry("foo", data) 86 | self.assertEqual(entry.shell, "/bin/bash") 87 | self.assertEqual(entry.dir, "/home/foo") 88 | self.assertEqual(entry.gecos, "") 89 | self.assertEqual(entry.passwd, "x") 90 | 91 | def testInvalidEntry(self): 92 | data = {"irrelevant_key": "bacon"} 93 | entry = self.parser._ReadEntry("foo", data) 94 | self.assertEqual(entry, None) 95 | 96 | 97 | class TestS3GroupMapParser(unittest.TestCase): 98 | def setUp(self): 99 | self.good_entry = group.GroupMapEntry() 100 | self.good_entry.name = "foo" 101 | self.good_entry.passwd = "x" 102 | self.good_entry.gid = 10 103 | self.good_entry.members = ["foo", "bar"] 104 | self.parser = s3source.S3GroupMapParser() 105 | 106 | def testGetMap(self): 107 | group_map = group.GroupMap() 108 | cache_info = StringIO( 109 | """[ 110 | { "Key": "foo", 111 | "Value": { 112 | "gid": 10, 113 | "members": "foo\\nbar", 114 | "irrelevant_key": "bacon" 115 | } 116 | } 117 | ]""" 118 | ) 119 | self.parser.GetMap(cache_info, group_map) 120 | self.assertEqual(self.good_entry, group_map.PopItem()) 121 | 122 | def testReadEntry(self): 123 | data = {"passwd": "x", "gid": "10", "members": "foo\nbar"} 124 | entry = self.parser._ReadEntry("foo", data) 125 | self.assertEqual(self.good_entry, entry) 126 | 127 | def testDefaultPasswd(self): 128 | data = {"gid": "10", "members": "foo\nbar"} 129 | entry = self.parser._ReadEntry("foo", data) 130 | self.assertEqual(self.good_entry, entry) 131 | 132 | def testNoMembers(self): 133 | data = {"gid": "10", "members": ""} 134 | entry = self.parser._ReadEntry("foo", data) 135 | self.assertEqual(entry.members, [""]) 136 | 137 | def testInvalidEntry(self): 138 | data = {"irrelevant_key": "bacon"} 139 | entry = self.parser._ReadEntry("foo", data) 140 | self.assertEqual(entry, None) 141 | 142 | 143 | class TestS3ShadowMapParser(unittest.TestCase): 144 | def setUp(self): 145 | self.good_entry = shadow.ShadowMapEntry() 146 | self.good_entry.name = "foo" 147 | self.good_entry.passwd = "*" 148 | self.good_entry.lstchg = 17246 149 | self.good_entry.min = 0 150 | self.good_entry.max = 99999 151 | self.good_entry.warn = 7 152 | self.parser = s3source.S3ShadowMapParser() 153 | 154 | def testGetMap(self): 155 | shadow_map = shadow.ShadowMap() 156 | cache_info = StringIO( 157 | """[ 158 | { "Key": "foo", 159 | "Value": { 160 | "passwd": "*", "lstchg": 17246, "min": 0, 161 | "max": 99999, "warn": 7 162 | } 163 | } 164 | ]""" 165 | ) 166 | self.parser.GetMap(cache_info, shadow_map) 167 | self.assertEqual(self.good_entry, shadow_map.PopItem()) 168 | 169 | def testReadEntry(self): 170 | data = {"passwd": "*", "lstchg": 17246, "min": 0, "max": 99999, "warn": 7} 171 | entry = self.parser._ReadEntry("foo", data) 172 | self.assertEqual(self.good_entry, entry) 173 | 174 | def testDefaultPasswd(self): 175 | data = {"lstchg": 17246, "min": 0, "max": 99999, "warn": 7} 176 | entry = self.parser._ReadEntry("foo", data) 177 | self.assertEqual(self.good_entry, entry) 178 | 179 | 180 | if __name__ == "__main__": 181 | unittest.main() 182 | -------------------------------------------------------------------------------- /nss_cache/sources/source.py: -------------------------------------------------------------------------------- 1 | # Copyright 2007 Google Inc. 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | """Base class of data source object for nss_cache.""" 17 | 18 | __author__ = ( 19 | "jaq@google.com (Jamie Wilkinson)", 20 | "vasilios@google.com (Vasilios Hoffman)", 21 | ) 22 | 23 | import logging 24 | 25 | from nss_cache import config 26 | from nss_cache import error 27 | 28 | 29 | class Source(object): 30 | """Abstract base class for map data sources.""" 31 | 32 | UPDATER = None 33 | 34 | def __init__(self, conf): 35 | """Initialise the Source object. 36 | 37 | Args: 38 | conf: A dictionary of key/value pairs. 39 | 40 | Raises: 41 | RuntimeError: object wasn't initialised with a dict 42 | """ 43 | if not isinstance(conf, dict): 44 | raise RuntimeError("Source constructor not passed a dictionary") 45 | 46 | self.conf = conf 47 | 48 | # create a logger for our children 49 | self.log = logging.getLogger(__name__) 50 | 51 | def GetMap(self, map_name, since=None, location=None): 52 | """Get a specific map from this source. 53 | 54 | Args: 55 | map_name: A string representation of the map you want 56 | since: optional timestamp for incremental query 57 | location: optional field used by automounts to indicate a specific map 58 | 59 | Returns: 60 | A Map child class for the map requested. 61 | 62 | Raises: 63 | UnsupportedMap: for unknown source maps 64 | """ 65 | if map_name == config.MAP_PASSWORD: 66 | return self.GetPasswdMap(since) 67 | elif map_name == config.MAP_SSHKEY: 68 | return self.GetSshkeyMap(since) 69 | elif map_name == config.MAP_GROUP: 70 | return self.GetGroupMap(since) 71 | elif map_name == config.MAP_SHADOW: 72 | return self.GetShadowMap(since) 73 | elif map_name == config.MAP_NETGROUP: 74 | return self.GetNetgroupMap(since) 75 | elif map_name == config.MAP_AUTOMOUNT: 76 | return self.GetAutomountMap(since, location=location) 77 | 78 | raise error.UnsupportedMap("Source can not fetch %s" % map_name) 79 | 80 | def GetAutomountMap(self, since=None, location=None): 81 | """Get an automount map from this source.""" 82 | raise NotImplementedError 83 | 84 | def GetAutomountMasterMap(self): 85 | """Get an automount map from this source.""" 86 | raise NotImplementedError 87 | 88 | def Verify(self): 89 | """Perform verification of the source availability. 90 | 91 | Attempt to open/connect or otherwise use the data source, and 92 | report if there are any problems. 93 | """ 94 | raise NotImplementedError 95 | 96 | 97 | class FileSource(object): 98 | """Abstract base class for file data sources.""" 99 | 100 | def __init__(self, conf): 101 | """Initialise the Source object. 102 | 103 | Args: 104 | conf: A dictionary of key/value pairs. 105 | 106 | Raises: 107 | RuntimeError: object wasn't initialised with a dict 108 | """ 109 | if not isinstance(conf, dict): 110 | raise RuntimeError("Source constructor not passed a dictionary") 111 | 112 | self.conf = conf 113 | 114 | # create a logger for our children 115 | self.log = logging.getLogger(__name__) 116 | 117 | def GetFile(self, map_name, dst_file, current_file, location=None): 118 | """Retrieve a file from this source. 119 | 120 | Args: 121 | map_name: A string representation of the map whose file you want 122 | dst_file: Temporary filename to write to. 123 | current_file: Path to the current cache. 124 | location: optional field used by automounts to indicate a specific map 125 | 126 | Returns: 127 | path to new file 128 | 129 | Raises: 130 | UnsupportedMap: for unknown source maps 131 | """ 132 | if map_name == config.MAP_PASSWORD: 133 | return self.GetPasswdFile(dst_file, current_file) 134 | elif map_name == config.MAP_GROUP: 135 | return self.GetGroupFile(dst_file, current_file) 136 | elif map_name == config.MAP_SHADOW: 137 | return self.GetShadowFile(dst_file, current_file) 138 | elif map_name == config.MAP_NETGROUP: 139 | return self.GetNetgroupFile(dst_file, current_file) 140 | elif map_name == config.MAP_AUTOMOUNT: 141 | return self.GetAutomountFile(dst_file, current_file, location=location) 142 | 143 | raise error.UnsupportedMap("Source can not fetch %s" % map_name) 144 | -------------------------------------------------------------------------------- /nss_cache/sources/source_factory.py: -------------------------------------------------------------------------------- 1 | # Copyright 2007 Google Inc. 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | """Factory for data source implementations.""" 17 | 18 | __author__ = ( 19 | "jaq@google.com (Jamie Wilkinson)", 20 | "vasilios@google.com (Vasilios Hoffman)", 21 | ) 22 | 23 | _source_implementations = {} 24 | 25 | 26 | def RegisterImplementation(source): 27 | """Register a Source implementation with the factory method. 28 | 29 | Sources being registered are expected to have a name attribute, 30 | unique to themselves. 31 | 32 | Child modules are expected to call this method in the file-level 33 | scope. 34 | 35 | Args: 36 | source: A class type that is a subclass of Source 37 | 38 | Returns: 39 | Nothing 40 | 41 | Raises: 42 | RuntimeError: no 'name' entry in this source. 43 | """ 44 | global _source_implementations 45 | if "name" not in source.__dict__: 46 | raise RuntimeError("'name' not defined in Source %r" % (source,)) 47 | 48 | _source_implementations[source.name] = source 49 | 50 | 51 | # Discover all the known implementations of sources. 52 | try: 53 | from nss_cache.sources import httpsource 54 | 55 | httpsource.RegisterImplementation(RegisterImplementation) 56 | except ImportError: 57 | pass 58 | 59 | try: 60 | from nss_cache.sources import ldapsource 61 | 62 | ldapsource.RegisterImplementation(RegisterImplementation) 63 | except ImportError: 64 | pass 65 | 66 | try: 67 | from nss_cache.sources import consulsource 68 | 69 | consulsource.RegisterImplementation(RegisterImplementation) 70 | except ImportError: 71 | pass 72 | 73 | try: 74 | from nss_cache.sources import s3source 75 | 76 | s3source.RegisterImplementation(RegisterImplementation) 77 | except ImportError: 78 | pass 79 | 80 | try: 81 | from nss_cache.sources import gcssource 82 | 83 | gcssource.RegisterImplementation(RegisterImplementation) 84 | except ImportError: 85 | pass 86 | 87 | 88 | def Create(conf): 89 | """Source creation factory method. 90 | 91 | Args: 92 | conf: a dictionary of configuration key/value pairs, including one 93 | required attribute 'name'. 94 | 95 | Returns: 96 | A Source instance. 97 | 98 | Raises: 99 | RuntimeError: no sources are registered with RegisterImplementation 100 | """ 101 | global _source_implementations 102 | if not _source_implementations: 103 | raise RuntimeError("no source implementations exist") 104 | 105 | source_name = conf["name"] 106 | 107 | if source_name not in list(_source_implementations.keys()): 108 | raise RuntimeError("source not implemented: %r" % (source_name,)) 109 | 110 | return _source_implementations[source_name](conf) 111 | -------------------------------------------------------------------------------- /nss_cache/sources/source_factory_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2007 Google Inc. 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | """Unit tests for sources/source.py.""" 17 | 18 | __author__ = "jaq@google.com (Jamie Wilkinson)" 19 | 20 | import unittest 21 | 22 | from nss_cache.sources import source 23 | from nss_cache.sources import source_factory 24 | 25 | 26 | class TestSourceFactory(unittest.TestCase): 27 | """Unit tests for the source factory.""" 28 | 29 | def testRegister(self): 30 | 31 | number_of_sources = len(source_factory._source_implementations) 32 | 33 | class DummySource(source.Source): 34 | name = "dummy" 35 | 36 | source_factory.RegisterImplementation(DummySource) 37 | 38 | self.assertEqual( 39 | number_of_sources + 1, len(source_factory._source_implementations) 40 | ) 41 | self.assertEqual(DummySource, source_factory._source_implementations["dummy"]) 42 | 43 | def testRegisterWithoutName(self): 44 | class DummySource(source.Source): 45 | pass 46 | 47 | self.assertRaises( 48 | RuntimeError, source_factory.RegisterImplementation, DummySource 49 | ) 50 | 51 | def testCreateWithNoImplementations(self): 52 | source_factory._source_implementations = {} 53 | self.assertRaises(RuntimeError, source_factory.Create, {}) 54 | 55 | def testCreate(self): 56 | class DummySource(source.Source): 57 | name = "dummy" 58 | 59 | source_factory.RegisterImplementation(DummySource) 60 | 61 | dummy_config = {"name": "dummy"} 62 | dummy_source = source_factory.Create(dummy_config) 63 | 64 | self.assertEqual(DummySource, type(dummy_source)) 65 | 66 | 67 | if __name__ == "__main__": 68 | unittest.main() 69 | -------------------------------------------------------------------------------- /nss_cache/sources/source_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2007 Google Inc. 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | """Unit tests for sources/source.py.""" 17 | 18 | __author__ = "jaq@google.com (Jamie Wilkinson)" 19 | 20 | import unittest 21 | 22 | from nss_cache.sources import source 23 | 24 | 25 | class TestSource(unittest.TestCase): 26 | """Unit tests for the Source class.""" 27 | 28 | def testCreateNoConfig(self): 29 | 30 | config = [] 31 | 32 | self.assertRaises(RuntimeError, source.Source, config) 33 | 34 | self.assertRaises(RuntimeError, source.Source, None) 35 | 36 | config = "foo" 37 | 38 | self.assertRaises(RuntimeError, source.Source, config) 39 | 40 | def testVerify(self): 41 | s = source.Source({}) 42 | self.assertRaises(NotImplementedError, s.Verify) 43 | 44 | 45 | if __name__ == "__main__": 46 | unittest.main() 47 | -------------------------------------------------------------------------------- /nss_cache/update/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/nsscache/66e3789910b2641e52707bba55d6e5d381069257/nss_cache/update/__init__.py -------------------------------------------------------------------------------- /nss_cache/update/updater_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2007 Google Inc. 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | """Unit tests for nss_cache/update/base.py.""" 17 | 18 | __author__ = ("vasilios@google.com (V Hoffman)", "jaq@google.com (Jamie Wilkinson)") 19 | 20 | import os 21 | import shutil 22 | import tempfile 23 | import time 24 | import unittest 25 | from unittest import mock 26 | 27 | from nss_cache import config 28 | from nss_cache.update import updater 29 | 30 | 31 | class TestUpdater(unittest.TestCase): 32 | """Unit tests for the Updater class.""" 33 | 34 | def setUp(self): 35 | super(TestUpdater, self).setUp() 36 | self.workdir = tempfile.mkdtemp() 37 | 38 | def tearDown(self): 39 | shutil.rmtree(self.workdir) 40 | super(TestUpdater, self).tearDown() 41 | 42 | def testTimestampDir(self): 43 | """We read and write timestamps to the specified directory.""" 44 | update_obj = updater.Updater(config.MAP_PASSWORD, self.workdir, {}) 45 | self.updater = updater 46 | update_time = 1199149400 47 | modify_time = 1199149200 48 | 49 | update_obj.WriteUpdateTimestamp(update_time) 50 | update_obj.WriteModifyTimestamp(modify_time) 51 | 52 | update_stamp = update_obj.GetUpdateTimestamp() 53 | modify_stamp = update_obj.GetModifyTimestamp() 54 | 55 | self.assertEqual( 56 | update_time, 57 | update_stamp, 58 | msg=( 59 | "retrieved a different update time than we stored: " 60 | "Expected: %r, observed: %r" % (update_time, update_stamp) 61 | ), 62 | ) 63 | self.assertEqual( 64 | modify_time, 65 | modify_stamp, 66 | msg=( 67 | "retrieved a different modify time than we stored: " 68 | "Expected %r, observed: %r" % (modify_time, modify_stamp) 69 | ), 70 | ) 71 | 72 | def testWriteWhenTimestampIsNone(self): 73 | update_obj = updater.Updater(config.MAP_PASSWORD, self.workdir, {}) 74 | self.assertEqual(True, update_obj.WriteUpdateTimestamp(None)) 75 | self.assertEqual(True, update_obj.WriteModifyTimestamp(None)) 76 | 77 | def testTimestampDefaultsToNone(self): 78 | """Missing or unreadable timestamps return None.""" 79 | update_obj = updater.Updater(config.MAP_PASSWORD, self.workdir, {}) 80 | self.updater = update_obj 81 | update_stamp = update_obj.GetUpdateTimestamp() 82 | modify_stamp = update_obj.GetModifyTimestamp() 83 | 84 | self.assertEqual(None, update_stamp, msg="update time did not default to None") 85 | self.assertEqual(None, modify_stamp, msg="modify time did not default to None") 86 | 87 | # touch a file, make it unreadable 88 | update_file = open(update_obj.update_file, "w") 89 | modify_file = open(update_obj.modify_file, "w") 90 | update_file.close() 91 | modify_file.close() 92 | os.chmod(update_obj.update_file, 0000) 93 | os.chmod(update_obj.modify_file, 0000) 94 | 95 | update_stamp = update_obj.GetUpdateTimestamp() 96 | modify_stamp = update_obj.GetModifyTimestamp() 97 | 98 | self.assertEqual( 99 | None, update_stamp, msg="unreadable update time did not default to None" 100 | ) 101 | self.assertEqual( 102 | None, modify_stamp, msg="unreadable modify time did not default to None" 103 | ) 104 | 105 | def testTimestampInTheFuture(self): 106 | """Timestamps in the future are turned into now.""" 107 | update_obj = updater.Updater(config.MAP_PASSWORD, self.workdir, {}) 108 | expected_time = 1 109 | update_time = 3601 110 | update_file = open(update_obj.update_file, "w") 111 | update_obj.WriteUpdateTimestamp(update_time) 112 | update_file.close() 113 | 114 | with mock.patch.object( 115 | update_obj, "_GetCurrentTime", return_value=expected_time 116 | ) as ct: 117 | self.assertEqual(expected_time, update_obj.GetUpdateTimestamp()) 118 | 119 | 120 | if __name__ == "__main__": 121 | unittest.main() 122 | -------------------------------------------------------------------------------- /nss_cache/util/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/nsscache/66e3789910b2641e52707bba55d6e5d381069257/nss_cache/util/__init__.py -------------------------------------------------------------------------------- /nss_cache/util/curl.py: -------------------------------------------------------------------------------- 1 | # Copyright 2010 Google Inc. 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | """Minor curl methods.""" 17 | 18 | __author__ = "blaedd@google.com (David MacKinnon)" 19 | 20 | import logging 21 | import pycurl 22 | from io import BytesIO 23 | 24 | from nss_cache import error 25 | 26 | 27 | def CurlFetch(url, conn=None, logger=None): 28 | if not logger: 29 | logger = logging 30 | 31 | if not conn: 32 | conn = pycurl.Curl() 33 | 34 | conn.setopt(pycurl.URL, url) 35 | conn.body = BytesIO() 36 | conn.headers = BytesIO() 37 | conn.setopt(pycurl.WRITEFUNCTION, conn.body.write) 38 | conn.setopt(pycurl.HEADERFUNCTION, conn.headers.write) 39 | try: 40 | conn.perform() 41 | except pycurl.error as e: 42 | HandleCurlError(e, logger) 43 | raise error.Error(e) 44 | resp_code = conn.getinfo(pycurl.RESPONSE_CODE) 45 | return (resp_code, conn.headers.getvalue().decode("utf-8"), conn.body.getvalue()) 46 | 47 | 48 | def HandleCurlError(e, logger=None): 49 | """Handle a curl exception. 50 | 51 | See http://curl.haxx.se/libcurl/c/libcurl-errors.html for a list of codes. 52 | 53 | Args: 54 | e: pycurl.error 55 | logger: logger object 56 | 57 | Raises: 58 | ConfigurationError: 59 | PermissionDenied: 60 | SourceUnavailable: 61 | Error: 62 | """ 63 | if not logger: 64 | logger = logging 65 | 66 | code = e.args[0] 67 | msg = e.args[1] 68 | 69 | # Config errors 70 | if code in ( 71 | pycurl.E_UNSUPPORTED_PROTOCOL, 72 | pycurl.E_URL_MALFORMAT, 73 | pycurl.E_SSL_ENGINE_NOTFOUND, 74 | pycurl.E_SSL_ENGINE_SETFAILED, 75 | pycurl.E_SSL_CACERT_BADFILE, 76 | ): 77 | raise error.ConfigurationError(msg) 78 | 79 | # Possibly transient errors, try again 80 | if code in ( 81 | pycurl.E_FAILED_INIT, 82 | pycurl.E_COULDNT_CONNECT, 83 | pycurl.E_PARTIAL_FILE, 84 | pycurl.E_WRITE_ERROR, 85 | pycurl.E_READ_ERROR, 86 | pycurl.E_OPERATION_TIMEOUTED, 87 | pycurl.E_SSL_CONNECT_ERROR, 88 | pycurl.E_COULDNT_RESOLVE_PROXY, 89 | pycurl.E_COULDNT_RESOLVE_HOST, 90 | pycurl.E_GOT_NOTHING, 91 | ): 92 | logger.debug("Possibly transient error: %s", msg) 93 | return 94 | 95 | # SSL issues 96 | if code in (pycurl.E_SSL_PEER_CERTIFICATE,): 97 | raise error.SourceUnavailable(msg) 98 | 99 | # Anything else 100 | raise error.Error(msg) 101 | -------------------------------------------------------------------------------- /nss_cache/util/file_formats.py: -------------------------------------------------------------------------------- 1 | # Copyright 2007 Google Inc. 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | """Parsing methods for file cache types.""" 17 | 18 | __author__ = ( 19 | "jaq@google.com (Jamie Wilkinson)", 20 | "vasilios@google.com (Vasilios Hoffman)", 21 | ) 22 | 23 | import logging 24 | 25 | from nss_cache.maps import automount 26 | from nss_cache.maps import group 27 | from nss_cache.maps import netgroup 28 | from nss_cache.maps import passwd 29 | from nss_cache.maps import shadow 30 | from nss_cache.maps import sshkey 31 | 32 | try: 33 | SetType = set 34 | except NameError: 35 | import sets 36 | 37 | SetType = sets.Set 38 | 39 | 40 | class FilesMapParser(object): 41 | """A base class for parsing nss_files module cache.""" 42 | 43 | def __init__(self): 44 | self.log = logging.getLogger(__name__) 45 | 46 | def GetMap(self, cache_info, data): 47 | """Returns a map from a cache. 48 | 49 | Args: 50 | cache_info: file like object containing the cache. 51 | data: a Map to populate. 52 | Returns: 53 | A child of Map containing the cache data. 54 | """ 55 | for line in cache_info: 56 | line = line.rstrip("\n") 57 | if not line or line[0] == "#": 58 | continue 59 | entry = self._ReadEntry(line) 60 | if entry is None: 61 | self.log.warning( 62 | "Could not create entry from line %r in cache, skipping", line 63 | ) 64 | continue 65 | if not data.Add(entry): 66 | self.log.warning( 67 | "Could not add entry %r read from line %r in cache", entry, line 68 | ) 69 | return data 70 | 71 | 72 | class FilesSshkeyMapParser(FilesMapParser): 73 | """Class for parsing nss_files module sshkey cache.""" 74 | 75 | def _ReadEntry(self, entry): 76 | """Return a SshkeyMapEntry from a record in the target cache.""" 77 | entry = entry.split(":") 78 | map_entry = sshkey.SshkeyMapEntry() 79 | # maps expect strict typing, so convert to int as appropriate. 80 | map_entry.name = entry[0] 81 | map_entry.sshkey = entry[1] 82 | return map_entry 83 | 84 | 85 | class FilesPasswdMapParser(FilesMapParser): 86 | """Class for parsing nss_files module passwd cache.""" 87 | 88 | def _ReadEntry(self, entry): 89 | """Return a PasswdMapEntry from a record in the target cache.""" 90 | entry = entry.split(":") 91 | map_entry = passwd.PasswdMapEntry() 92 | # maps expect strict typing, so convert to int as appropriate. 93 | map_entry.name = entry[0] 94 | map_entry.passwd = entry[1] 95 | map_entry.uid = int(entry[2]) 96 | map_entry.gid = int(entry[3]) 97 | map_entry.gecos = entry[4] 98 | map_entry.dir = entry[5] 99 | map_entry.shell = entry[6] 100 | return map_entry 101 | 102 | 103 | class FilesGroupMapParser(FilesMapParser): 104 | """Class for parsing a nss_files module group cache.""" 105 | 106 | def _ReadEntry(self, line): 107 | """Return a GroupMapEntry from a record in the target cache.""" 108 | line = line.split(":") 109 | map_entry = group.GroupMapEntry() 110 | # map entries expect strict typing, so convert as appropriate 111 | map_entry.name = line[0] 112 | map_entry.passwd = line[1] 113 | map_entry.gid = int(line[2]) 114 | map_entry.members = line[3].split(",") 115 | return map_entry 116 | 117 | 118 | class FilesShadowMapParser(FilesMapParser): 119 | """Class for parsing a nss_files module shadow cache.""" 120 | 121 | def _ReadEntry(self, line): 122 | """Return a ShadowMapEntry from a record in the target cache.""" 123 | line = line.split(":") 124 | map_entry = shadow.ShadowMapEntry() 125 | # map entries expect strict typing, so convert as appropriate 126 | map_entry.name = line[0] 127 | map_entry.passwd = line[1] 128 | if line[2]: 129 | map_entry.lstchg = int(line[2]) 130 | if line[3]: 131 | map_entry.min = int(line[3]) 132 | if line[4]: 133 | map_entry.max = int(line[4]) 134 | if line[5]: 135 | map_entry.warn = int(line[5]) 136 | if line[6]: 137 | map_entry.inact = int(line[6]) 138 | if line[7]: 139 | map_entry.expire = int(line[7]) 140 | if line[8]: 141 | map_entry.flag = int(line[8]) 142 | return map_entry 143 | 144 | 145 | class FilesNetgroupMapParser(FilesMapParser): 146 | """Class for parsing a nss_files module netgroup cache.""" 147 | 148 | def _ReadEntry(self, line): 149 | """Return a NetgroupMapEntry from a record in the target cache.""" 150 | map_entry = netgroup.NetgroupMapEntry() 151 | 152 | # the first word is our name, but since the whole line is space delimited 153 | # avoid .split(' ') since groups can have thousands of members. 154 | index = line.find(" ") 155 | 156 | if index == -1: 157 | if line: 158 | # empty group is OK, as long as the line isn't blank 159 | map_entry.name = line 160 | return map_entry 161 | raise RuntimeError("Failed to parse entry: %s" % line) 162 | 163 | map_entry.name = line[0:index] 164 | 165 | # the rest is our entries, and for better or for worse this preserves extra 166 | # leading spaces 167 | map_entry.entries = line[index + 1 :] 168 | 169 | return map_entry 170 | 171 | 172 | class FilesAutomountMapParser(FilesMapParser): 173 | """Class for parsing a nss_files module automount cache.""" 174 | 175 | def _ReadEntry(self, line): 176 | """Return an AutomountMapEntry from a record in the target cache. 177 | 178 | Args: 179 | line: A string from a file cache. 180 | 181 | Returns: 182 | An AutomountMapEntry if the line is successfully parsed, None otherwise. 183 | """ 184 | line = line.split() 185 | map_entry = automount.AutomountMapEntry() 186 | try: 187 | map_entry.key = line[0] 188 | if len(line) > 2: 189 | map_entry.options = line[1] 190 | map_entry.location = line[2] 191 | else: 192 | map_entry.location = line[1] 193 | except IndexError: 194 | return None 195 | return map_entry 196 | -------------------------------------------------------------------------------- /nss_cache/util/file_formats_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2007 Google Inc. 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | """Unit tests for nss_cache/util/file_formats.py.""" 17 | 18 | __author__ = ( 19 | "jaq@google.com (Jamie Wilkinson)", 20 | "vasilios@google.com (Vasilios Hoffman)", 21 | ) 22 | 23 | import unittest 24 | 25 | from nss_cache.util import file_formats 26 | 27 | 28 | class TestFilesUtils(unittest.TestCase): 29 | def testReadPasswdEntry(self): 30 | """We correctly parse a typical entry in /etc/passwd format.""" 31 | parser = file_formats.FilesPasswdMapParser() 32 | file_entry = "root:x:0:0:Rootsy:/root:/bin/bash" 33 | map_entry = parser._ReadEntry(file_entry) 34 | 35 | self.assertEqual(map_entry.name, "root") 36 | self.assertEqual(map_entry.passwd, "x") 37 | self.assertEqual(map_entry.uid, 0) 38 | self.assertEqual(map_entry.gid, 0) 39 | self.assertEqual(map_entry.gecos, "Rootsy") 40 | self.assertEqual(map_entry.dir, "/root") 41 | self.assertEqual(map_entry.shell, "/bin/bash") 42 | 43 | def testReadGroupEntry(self): 44 | """We correctly parse a typical entry in /etc/group format.""" 45 | parser = file_formats.FilesGroupMapParser() 46 | file_entry = "root:x:0:zero_cool,acid_burn" 47 | map_entry = parser._ReadEntry(file_entry) 48 | 49 | self.assertEqual(map_entry.name, "root") 50 | self.assertEqual(map_entry.passwd, "x") 51 | self.assertEqual(map_entry.gid, 0) 52 | self.assertEqual(map_entry.members, ["zero_cool", "acid_burn"]) 53 | 54 | def testReadShadowEntry(self): 55 | """We correctly parse a typical entry in /etc/shadow format.""" 56 | parser = file_formats.FilesShadowMapParser() 57 | file_entry = "root:$1$zomgmd5support:::::::" 58 | map_entry = parser._ReadEntry(file_entry) 59 | 60 | self.assertEqual(map_entry.name, "root") 61 | self.assertEqual(map_entry.passwd, "$1$zomgmd5support") 62 | self.assertEqual(map_entry.lstchg, None) 63 | self.assertEqual(map_entry.min, None) 64 | self.assertEqual(map_entry.max, None) 65 | self.assertEqual(map_entry.warn, None) 66 | self.assertEqual(map_entry.inact, None) 67 | self.assertEqual(map_entry.expire, None) 68 | self.assertEqual(map_entry.flag, None) 69 | 70 | def testReadNetgroupEntry(self): 71 | """We correctly parse a typical entry in /etc/netgroup format.""" 72 | parser = file_formats.FilesNetgroupMapParser() 73 | file_entry = "administrators unix_admins noc_monkeys (-,zero_cool,)" 74 | map_entry = parser._ReadEntry(file_entry) 75 | 76 | self.assertEqual(map_entry.name, "administrators") 77 | self.assertEqual(map_entry.entries, "unix_admins noc_monkeys (-,zero_cool,)") 78 | 79 | def testReadEmptyNetgroupEntry(self): 80 | """We correctly parse a memberless netgroup entry.""" 81 | parser = file_formats.FilesNetgroupMapParser() 82 | file_entry = "administrators" 83 | map_entry = parser._ReadEntry(file_entry) 84 | 85 | self.assertEqual(map_entry.name, "administrators") 86 | self.assertEqual(map_entry.entries, "") 87 | 88 | def testReadAutomountEntry(self): 89 | """We correctly parse a typical entry in /etc/auto.* format.""" 90 | parser = file_formats.FilesAutomountMapParser() 91 | file_entry = "scratch -tcp,rw,intr,bg fileserver:/scratch" 92 | map_entry = parser._ReadEntry(file_entry) 93 | 94 | self.assertEqual(map_entry.key, "scratch") 95 | self.assertEqual(map_entry.options, "-tcp,rw,intr,bg") 96 | self.assertEqual(map_entry.location, "fileserver:/scratch") 97 | 98 | def testReadAutmountEntryWithExtraWhitespace(self): 99 | """Extra whitespace doesn't break the parsing.""" 100 | parser = file_formats.FilesAutomountMapParser() 101 | file_entry = "scratch fileserver:/scratch" 102 | map_entry = parser._ReadEntry(file_entry) 103 | 104 | self.assertEqual(map_entry.key, "scratch") 105 | self.assertEqual(map_entry.options, None) 106 | self.assertEqual(map_entry.location, "fileserver:/scratch") 107 | 108 | def testReadBadAutomountEntry(self): 109 | """Cope with empty data.""" 110 | parser = file_formats.FilesAutomountMapParser() 111 | file_entry = "" 112 | map_entry = parser._ReadEntry(file_entry) 113 | self.assertEqual(None, map_entry) 114 | 115 | 116 | if __name__ == "__main__": 117 | unittest.main() 118 | -------------------------------------------------------------------------------- /nss_cache/util/timestamps.py: -------------------------------------------------------------------------------- 1 | # Copyright 2011 Google Inc. 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | """Timestamp handling routines.""" 17 | 18 | __author__ = "jaq@google.com (Jamie Wilkinson)" 19 | 20 | import logging 21 | import os.path 22 | import tempfile 23 | import time 24 | import stat 25 | import datetime 26 | 27 | 28 | def ReadTimestamp(filename): 29 | """Return a timestamp from a file. 30 | 31 | The timestamp file format is a single line, containing a string in the 32 | ISO-8601 format YYYY-MM-DDThh:mm:ssZ (i.e. UTC time). We do not support 33 | all ISO-8601 formats for reasons of convenience in the code. 34 | 35 | Timestamps internal to nss_cache deliberately do not carry milliseconds. 36 | 37 | Args: 38 | filename: A String naming the file to read from. 39 | 40 | Returns: 41 | A time.struct_time, or None if the timestamp file doesn't 42 | exist or has errors. 43 | """ 44 | if not os.path.exists(filename): 45 | return None 46 | 47 | try: 48 | timestamp_file = open(filename, "r") 49 | timestamp_string = timestamp_file.read().strip() 50 | except IOError as e: 51 | logging.warning("error opening timestamp file: %s", e) 52 | timestamp_string = None 53 | else: 54 | timestamp_file.close() 55 | 56 | logging.debug("read timestamp %s from file %r", timestamp_string, filename) 57 | 58 | if timestamp_string is not None: 59 | try: 60 | # Append UTC to force the timezone to parse the string in. 61 | timestamp = time.strptime( 62 | timestamp_string + " UTC", "%Y-%m-%dT%H:%M:%SZ %Z" 63 | ) 64 | except ValueError as e: 65 | logging.error("cannot parse timestamp file %r: %s", filename, e) 66 | timestamp = None 67 | else: 68 | timestamp = None 69 | 70 | logging.debug("Timestamp is: %r", timestamp) 71 | now = time.gmtime() 72 | logging.debug(" Now is: %r", now) 73 | if timestamp > now: 74 | logging.warning( 75 | "timestamp %r (%r) from %r is in the future, now is %r", 76 | timestamp_string, 77 | time.mktime(timestamp), 78 | filename, 79 | time.mktime(now), 80 | ) 81 | if time.mktime(timestamp) - time.mktime(now) >= 60 * 60: 82 | logging.info("Resetting timestamp to now.") 83 | timestamp = now 84 | 85 | return timestamp 86 | 87 | 88 | def WriteTimestamp(timestamp, filename): 89 | """Write a given timestamp out to a file, converting to the ISO-8601 90 | format. 91 | 92 | We convert internal timestamp format (epoch) to ISO-8601 format, i.e. 93 | YYYY-MM-DDThh:mm:ssZ which is basically UTC time, then write it out to a 94 | file. 95 | 96 | Args: 97 | timestamp: A struct time.struct_time or time tuple. 98 | filename: A String naming the file to write to. 99 | 100 | Returns: 101 | A boolean indicating success of write. 102 | """ 103 | # TODO(jaq): hack 104 | if timestamp is None: 105 | return True 106 | 107 | timestamp_dir = os.path.dirname(filename) 108 | 109 | (filedesc, temp_filename) = tempfile.mkstemp( 110 | prefix="nsscache-update-", dir=timestamp_dir 111 | ) 112 | 113 | time_string = time.strftime("%Y-%m-%dT%H:%M:%SZ", timestamp) 114 | 115 | try: 116 | os.write(filedesc, b"%s\n" % time_string.encode()) 117 | os.fsync(filedesc) 118 | os.close(filedesc) 119 | except OSError: 120 | os.unlink(temp_filename) 121 | logging.warning("writing timestamp failed!") 122 | return False 123 | 124 | os.chmod(temp_filename, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH) 125 | os.rename(temp_filename, filename) 126 | logging.debug("wrote timestamp %s to file %r", time_string, filename) 127 | return True 128 | 129 | 130 | def FromTimestampToDateTime(ts): 131 | """Converts internal nss_cache timestamp to datetime object. 132 | 133 | Args: 134 | ts: number of seconds since epoch 135 | Returns: 136 | datetime object 137 | """ 138 | return datetime.datetime.utcfromtimestamp(ts) 139 | 140 | 141 | def FromDateTimeToTimestamp(datetime_obj): 142 | """Converts datetime object to internal nss_cache timestamp. 143 | 144 | Args: 145 | datetime object 146 | Returns: 147 | number of seconds since epoch 148 | """ 149 | dt = datetime_obj.replace(tzinfo=None) 150 | return int((dt - datetime.datetime(1970, 1, 1)).total_seconds()) 151 | -------------------------------------------------------------------------------- /nss_cache/util/timestamps_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2011 Google Inc. 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | """Unit tests for nss_cache/util/timestamps.py.""" 17 | 18 | __author__ = "jaq@google.com (Jamie Wilkinson)" 19 | 20 | import datetime 21 | from datetime import timezone 22 | import os 23 | import shutil 24 | import tempfile 25 | import time 26 | import unittest 27 | from unittest import mock 28 | 29 | from nss_cache.util import timestamps 30 | 31 | 32 | class TestTimestamps(unittest.TestCase): 33 | def setUp(self): 34 | super(TestTimestamps, self).setUp() 35 | self.workdir = tempfile.mkdtemp() 36 | 37 | def tearDown(self): 38 | super(TestTimestamps, self).tearDown() 39 | shutil.rmtree(self.workdir) 40 | 41 | def testReadTimestamp(self): 42 | ts_filename = os.path.join(self.workdir, "tsr") 43 | ts_file = open(ts_filename, "w") 44 | ts_file.write("1970-01-01T00:00:01Z\n") 45 | ts_file.close() 46 | 47 | ts = timestamps.ReadTimestamp(ts_filename) 48 | self.assertEqual(time.gmtime(1), ts) 49 | 50 | def testReadTimestamp(self): 51 | # TZ=UTC date -d @1306428781 52 | # Thu May 26 16:53:01 UTC 2011 53 | ts_filename = os.path.join(self.workdir, "tsr") 54 | ts_file = open(ts_filename, "w") 55 | ts_file.write("2011-05-26T16:53:01Z\n") 56 | ts_file.close() 57 | 58 | ts = timestamps.ReadTimestamp(ts_filename) 59 | self.assertEqual(time.gmtime(1306428781), ts) 60 | 61 | def testReadTimestampInFuture(self): 62 | ts_filename = os.path.join(self.workdir, "tsr") 63 | ts_file = open(ts_filename, "w") 64 | ts_file.write("2011-05-26T16:02:00Z") 65 | ts_file.close() 66 | 67 | now = time.gmtime(1) 68 | with mock.patch("time.gmtime") as gmtime: 69 | gmtime.return_value = now 70 | ts = timestamps.ReadTimestamp(ts_filename) 71 | self.assertEqual(now, ts) 72 | 73 | def testWriteTimestamp(self): 74 | ts_filename = os.path.join(self.workdir, "tsw") 75 | 76 | good_ts = time.gmtime(1) 77 | timestamps.WriteTimestamp(good_ts, ts_filename) 78 | 79 | self.assertEqual(good_ts, timestamps.ReadTimestamp(ts_filename)) 80 | 81 | ts_file = open(ts_filename, "r") 82 | self.assertEqual("1970-01-01T00:00:01Z\n", ts_file.read()) 83 | ts_file.close() 84 | 85 | def testTimestampToDateTime(self): 86 | now = datetime.datetime.now(timezone.utc) 87 | self.assertEqual( 88 | timestamps.FromTimestampToDateTime(now.timestamp()), 89 | now.replace(tzinfo=None), 90 | ) 91 | 92 | def testDateTimeToTimestamp(self): 93 | now = datetime.datetime.now(timezone.utc) 94 | self.assertEqual( 95 | now.replace(microsecond=0).timestamp(), 96 | timestamps.FromDateTimeToTimestamp(now), 97 | ) 98 | 99 | 100 | if __name__ == "__main__": 101 | unittest.main() 102 | -------------------------------------------------------------------------------- /nsscache: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # Copyright 2007 Google Inc. 4 | # 5 | # This program is free software; you can redistribute it and/or 6 | # modify it under the terms of the GNU General Public License 7 | # as published by the Free Software Foundation; either version 2 8 | # of the License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software Foundation, 17 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 | """Executable frontend to nss_cache.""" 19 | 20 | __author__ = ('jaq@google.com (Jamie Wilkinson)', 21 | 'vasilios@google.com (Vasilios Hoffman)') 22 | 23 | import os 24 | import sys 25 | import time 26 | 27 | from nss_cache import app 28 | 29 | if __name__ == '__main__': 30 | nsscache_app = app.NssCacheApp() 31 | start_time = time.process_time() 32 | return_value = nsscache_app.Run(sys.argv[1:], os.environ) 33 | end_time = time.process_time() 34 | nsscache_app.log.info('Exiting nsscache with value %d runtime %f', 35 | return_value, end_time - start_time) 36 | sys.exit(return_value) 37 | -------------------------------------------------------------------------------- /nsscache.1: -------------------------------------------------------------------------------- 1 | .TH NSSCACHE 1 2023-06-03 "nsscache 0.49" "User Commands" 2 | .SH NAME 3 | nsscache \- synchronise a local NSS cache with an upstream data source 4 | .SH SYNOPSIS 5 | .B nsscache 6 | [\fIglobal options\fR] \fIcommand \fR[\fIcommand options\fR] 7 | .SH DESCRIPTION 8 | .B nsscache 9 | synchronises a local NSS cache against a remote data source. 10 | This approach allows the administrator to separate the network from 11 | the NSS lookup codepath, improving speed and reliability of name 12 | services. 13 | .SH OPTIONS 14 | Global options alter general program behaviour: 15 | .TP 16 | \fB\-v\fR, \fB\-\-verbose\fR 17 | enable verbose output 18 | .TP 19 | \fB\-d\fR, \fB\-\-debug\fR 20 | enable debugging output 21 | .TP 22 | \fB\-c\fR \fIFILE\fR, \fB\-\-config\-file\fR=\fIFILE\fR 23 | read configuration from FILE 24 | .TP 25 | \fB\-\-version\fR 26 | show program's version number and exit 27 | .TP 28 | \fB\-h\fR, \fB\-\-help\fR 29 | show this help message and exit 30 | .SH COMMANDS 31 | .SS update 32 | Performs an update of the configured caches from the configured sources. 33 | .TP 34 | \fB\-f\fR, \fB\-\-full\fR 35 | force a full update from the data source 36 | .TP 37 | \fB\-\-force\fR 38 | force the update, overriding any safeguards and checks that would 39 | otherwise prevent the update from occurring. e.g. normally empty 40 | results from the data source are ignored as bogus -- this option will 41 | instruct the program to ignore its intuition and use the empty map 42 | .TP 43 | \fB\-m\fR \fIMAPS\fR, \fB\-\-map\fR=\fIMAPS\fR 44 | NSS map to operate on, can be supplied multiple times 45 | .TP 46 | \fB\-h\fR, \fB\-\-help\fR 47 | show help for the 48 | .B update 49 | command 50 | .SS verify 51 | Perform verification of the built caches and validation of the 52 | system NSS configuration. 53 | .TP 54 | \fB\-m\fR \fIMAPS\fR, \fB\-\-map\fR=\fIMAPS\fR 55 | NSS map to operate on, can be supplied multiple times 56 | .TP 57 | \fB\-h\fR, \fB\-\-help\fR 58 | show help for the 59 | .B verify 60 | command 61 | .SS status 62 | Show the last update time of each configured cache, and other 63 | metrics, optionally in a machine-readable format. 64 | .TP 65 | \fB\-m\fR \fIMAPS\fR, \fB\-\-map\fR=\fIMAPS\fR 66 | NSS map to operate on, can be supplied multiple times 67 | .TP 68 | \fB\-h\fR, \fB\-\-help\fR 69 | show help for the 70 | .B status 71 | command 72 | .SS repair 73 | Verify that the configuration is correct, that the source is 74 | reachable, then perform a full synchronisation of the cache. 75 | .TP 76 | \fB\-m\fR \fIMAPS\fR, \fB\-\-map\fR=\fIMAPS\fR 77 | NSS map to operate on, can be supplied multiple times 78 | .TP 79 | \fB\-h\fR, \fB\-\-help\fR 80 | show help for the 81 | .B repair 82 | command 83 | .SS help 84 | Shows online help for each command. 85 | .SH "SEE ALSO" 86 | .TP 87 | \fInsscache.conf\fP(5) 88 | .TP 89 | \fInsswitch.conf\fP(5) 90 | .SH FILES 91 | .TP 92 | \fI\|/etc/nsscache.conf\|\fP 93 | The system-wide configuration file 94 | .TP 95 | \fI\|/etc/nsswitch.conf\|\fP 96 | The system name service switch configuration file 97 | .SH AUTHOR 98 | Written by Jamie Wilkinson (jaq@google.com) and Vasilios Hoffman (vasilios@google.com). 99 | .TP 100 | The source code lives at https://github.com/google/nsscache 101 | .SH COPYRIGHT 102 | Copyright \(co 2007 Google, Inc. 103 | .br 104 | This is free software; see the source for copying conditions. There is NO 105 | warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 106 | -------------------------------------------------------------------------------- /nsscache.conf: -------------------------------------------------------------------------------- 1 | # Example /etc/nsscache.conf - configuration for nsscache 2 | # 3 | # nsscache loads a config file from the environment variable NSSCACHE_CONFIG 4 | # 5 | # By default this is /etc/nsscache.conf 6 | # 7 | # Commented values are overrideable defaults, uncommented values 8 | # require you to set them. 9 | 10 | [DEFAULT] 11 | 12 | # Default NSS data source module name 13 | source = ldap 14 | 15 | # Default NSS data cache module name; 'files' is compatible with the 16 | # libnss-cache NSS module. 17 | cache = files 18 | 19 | # NSS maps to be cached 20 | maps = passwd, group, shadow, netgroup, automount 21 | 22 | # Directory to store our update/modify timestamps 23 | timestamp_dir = /var/lib/nsscache 24 | 25 | # Lockfile to use for update/repair operations 26 | #lockfile = /var/run/nsscache 27 | 28 | # Defaults for specific modules; prefaced with "modulename_" 29 | 30 | ## 31 | # ldap module defaults. 32 | # 33 | 34 | # Enable to connect to Active Directory. If enabled (set to 1), 35 | # default Active Directory attributes will be used for mapping. 36 | # Leave disabled if connecting to openldap. 37 | #ldap_ad = 1 38 | 39 | # LDAP URI to query for NSS data 40 | ldap_uri = ldaps://ldap 41 | 42 | # Base for LDAP searches 43 | ldap_base = ou=people,dc=example,dc=com 44 | 45 | # Default LDAP search filter for maps 46 | ldap_filter = (objectclass=posixAccount) 47 | 48 | # Default LDAP search scope 49 | #ldap_scope = one 50 | 51 | # Default LDAP BIND DN, empty string is an anonymous bind 52 | #ldap_bind_dn = "" 53 | 54 | # Default LDAP password, empty DN and empty password is used for 55 | # anonymous binds 56 | #ldap_bind_password = "" 57 | 58 | # Default timelimit for LDAP queries, in seconds. 59 | # The query will block for this number of seconds, or indefinitely if negative. 60 | #ldap_timelimit = -1 61 | 62 | # Default number of retry attempts 63 | #ldap_retry_max = 3 64 | 65 | # Default delay in between retry attempts 66 | #ldap_retry_delay = 5 67 | 68 | # Default setting for requiring tls certificates, one of: 69 | # never, hard, demand, allow, try 70 | #ldap_tls_require_cert = 'demand' 71 | 72 | # Default directoy for trusted CAs 73 | #ldap_tls_cacertdir = '/usr/share/ssl' 74 | 75 | # Default filename for trusted CAs 76 | #ldap_tls_cacertfile = '/usr/share/ssl/cert.pem' 77 | 78 | # If you wish to use mTLS, set these to the paths of the TLS certificate and key. 79 | #ldap_tls_certfile = '' 80 | #ldap_tls_keyfile = '' 81 | 82 | # Should we issue STARTTLS? 83 | #ldap_tls_starttls = 1 84 | 85 | # Default uid-like attribute 86 | #ldap_uidattr = 'uid' 87 | 88 | # If connecting to openldap, uidNumber and gidNumber 89 | # will be used for mapping. If enabled (set to 1), 90 | # the relative identifier (RID) will be used instead. 91 | # Consider using this for Samba4 AD. 92 | #ldap_use_rid = 0 93 | 94 | # Default Offset option to map uidNumber and gidNumber to higher number. 95 | #ldap_offset = 10000 96 | 97 | # A Python regex to extract uid components from the uid-like attribute. 98 | # All matching groups are concatenated without spaces. 99 | # For example: '(.*)@example.com' would return a uid to the left of 100 | # the @example.com domain. Default is no regex. 101 | #ldap_uidregex = '' 102 | 103 | # A Python regex to extract group member components from the member or 104 | # memberOf attributes. All matching groups are concatenated without spaces. 105 | # For example: '(.*)@example.com' would return a member without the 106 | # the @example.com domain. Default is no regex. 107 | #ldap_groupregex = '' 108 | 109 | # Replace all users' shells with the specified one. 110 | # Enable for Active Directory since the loginShell 111 | # attribute is not present by default. 112 | #ldap_override_shell='/bin/bash' 113 | 114 | # Set directory for all users in passwd under /home. 115 | #ldap_home_dir = 1 116 | 117 | # Default uses rfc2307 schema. If rfc2307bis (groups stored as a list of DNs 118 | # in 'member' attr), set this to 1 119 | #ldap_rfc2307bis = 0 120 | 121 | # Default uses rfc2307 schema. If rfc2307bis_alt (groups stored as a list of DNs 122 | # in 'uniqueMember' attr), set this to 1 123 | #ldap_rfc2307bis_alt = 0 124 | 125 | # Debug logging 126 | #ldap_debug = 3 127 | 128 | # SASL 129 | # Use SASL for authentication 130 | #ldap_use_sasl = False 131 | 132 | # SASL mechanism. Only 'gssapi' is supported now 133 | #ldap_sasl_mech = 'gssapi' 134 | #ldap_sasl_authzid = '' 135 | 136 | ## 137 | # files module defaults 138 | 139 | # Directory to store the plain text files 140 | files_dir = /etc 141 | 142 | # Suffix used on the files module database files 143 | files_cache_filename_suffix = cache 144 | 145 | ### 146 | # Optional per-map sections, if present they will override the above 147 | # defaults. The examples below show you some common values to override 148 | # 149 | # [passwd] 150 | # 151 | # ldap_base = ou=people,dc=example,dc=com 152 | 153 | [group] 154 | 155 | ldap_base = ou=group,dc=example,dc=com 156 | ldap_filter = (objectclass=posixGroup) 157 | # If ldap_nested_groups is enabled, any groups are members of other groups 158 | # will be expanded recursively. 159 | # Note: This will only work with full updates. Incremental updates will not 160 | # propagate changes in child groups to their parents. 161 | # ldap_nested_groups = 1 162 | 163 | [shadow] 164 | 165 | ldap_filter = (objectclass=shadowAccount) 166 | 167 | [netgroup] 168 | 169 | ldap_base = ou=netgroup,dc=example,dc=com 170 | ldap_filter = (objectclass=nisNetgroup) 171 | files_cache_filename_suffix = 172 | 173 | [automount] 174 | 175 | ldap_base = ou=automounts,dc=example,dc=com 176 | files_cache_filename_suffix = 177 | cache = files 178 | 179 | # Files module has an option that lets you leave the local master map alone 180 | # (e.g. /etc/auto.master) so that maps can be enabled/disabled locally. 181 | # 182 | # This also causes nsscache to limit automount updates to only the maps which 183 | # are defined both in the local master map (/etc/auto.master) and in the source 184 | # master map -- versus pulling local copies of all maps defined in the source, 185 | # regardless. Effectively this makes for local control of which automount maps 186 | # are used and updated. 187 | # 188 | # files_local_automount_master = no 189 | 190 | ## 191 | ## SSH Keys stored in LDAP 192 | ## 193 | # For SSH keys stored in LDAP under the sshPublicKey attribute. 194 | # sshd_config should contain a config option for AuthorizedKeysCommand that 195 | # runs a script like: 196 | # 197 | # awk -F: -v name="$1" '$0 ~ name { print $2 }' /etc/sshkey.cache | \ 198 | # tr -d "[']" | \ 199 | # sed -e 's/, /\n/g' 200 | # 201 | # A featureful example is in examples/authorized-keys-command.py 202 | 203 | #[sshkey] 204 | # 205 | #ldap_base = ou=people,dc=yourdomain,dc=com 206 | 207 | [suffix] 208 | prefix = "" 209 | suffix = "" 210 | -------------------------------------------------------------------------------- /nsscache.cron: -------------------------------------------------------------------------------- 1 | # /etc/cron.d/nsscache: crontab entries for the nsscache package 2 | # 3 | # Example crontab for nsscache. 4 | # Replace the %% text with real values before deploying. 5 | 6 | SHELL=/bin/sh 7 | PATH=/usr/bin 8 | MAILTO="" 9 | NSSCACHE=/usr/bin/nsscache 10 | 11 | # disable /etc/ldap.conf defaults like the 2 minute timeout. 12 | LDAPNOINIT=1 13 | 14 | # update the cache 15 minutely 15 | %MINUTE15%-59/15 * * * * root $NSSCACHE -v update --sleep %SECONDS% 16 | 17 | # perform a full update once a day, at a time chosen during package 18 | # configuration (between 2AM and 5AM) 19 | %MINUTE% %HOUR% * * * root $NSSCACHE -v update --full 20 | -------------------------------------------------------------------------------- /nsscache.sh: -------------------------------------------------------------------------------- 1 | #/usr/bin/env bash 2 | 3 | _nsscache () 4 | { 5 | local cur prev options commands update_options other_options maps 6 | 7 | COMPREPLY=() 8 | cur="${COMP_WORDS[COMP_CWORD]}" 9 | prev="${COMP_WORDS[COMP_CWORD-1]}" 10 | 11 | options='-v --verbose -d --debug -c --config-file --version -h --help' 12 | commands='update verify status repair help' 13 | 14 | update_options='-f --full --force -m --map -h --help' 15 | other_options='-m --map -h --help' 16 | 17 | maps="passwd group shadow" 18 | 19 | case "${COMP_CWORD}" in 20 | 1) 21 | COMPREPLY=( $(compgen -W "${options} ${commands}" -- "${cur}" )) 22 | ;; 23 | 2) 24 | case "${prev}" in 25 | update) 26 | COMPREPLY=( $( compgen -W "${update_options}" -- "${cur}" )) 27 | return 0 28 | ;; 29 | verify|status|repair) 30 | COMPREPLY=( $( compgen -W "${other_options}" -- "${cur}" )) 31 | return 0 32 | ;; 33 | -c|--config-file) 34 | COMPREPLY=( $( compgen -o plusdirs -f -- "${cur}" )) 35 | return 0 36 | ;; 37 | -h|--help|--version|help) 38 | return 0 39 | ;; 40 | -v|--verbose|-d|--debug ) 41 | COMPREPLY=( $( compgen -W "${commands}" -- "${cur}" )) 42 | return 0 43 | ;; 44 | esac 45 | ;; 46 | 3) 47 | case "${prev}" in 48 | update) 49 | COMPREPLY=( $( compgen -W "${update_options}" -- "${cur}" )) 50 | return 0 51 | ;; 52 | verify|status|repair) 53 | COMPREPLY=( $( compgen -W "${other_options}" -- "${cur}" )) 54 | return 0 55 | ;; 56 | -m|--map) 57 | COMPREPLY=( $( compgen -W "${maps}" -- "${cur}" )) 58 | return 0 59 | ;; 60 | -f|--full|--force) 61 | COMPREPLY=() 62 | return 0 63 | ;; 64 | *) 65 | COMPREPLY=( $( compgen -W "${commands}" -- "${cur}" )) 66 | return 0 67 | ;; 68 | esac 69 | ;; 70 | 4) 71 | case "${prev}" in 72 | update) 73 | COMPREPLY=( $( compgen -W "${update_options}" -- "${cur}" )) 74 | return 0 75 | ;; 76 | verify|status|repair) 77 | COMPREPLY=( $( compgen -W "${other_options}" -- "${cur}" )) 78 | return 0 79 | ;; 80 | -m|--map) 81 | COMPREPLY=( $( compgen -W "${maps}" -- "${cur}" )) 82 | return 0 83 | ;; 84 | -f|--full|--force) 85 | COMPREPLY=() 86 | return 0 87 | ;; 88 | *) 89 | COMPREPLY=( $( compgen -W "${commands}" -- "${cur}" )) 90 | return 0 91 | ;; 92 | esac 93 | ;; 94 | 5) 95 | case "${prev}" in 96 | -m|--map) 97 | COMPREPLY=( $( compgen -W "${maps}" -- "${cur}" )) 98 | return 0 99 | ;; 100 | esac 101 | ;; 102 | *) 103 | COMPREPLY=() 104 | return 0 105 | ;; 106 | esac 107 | } 108 | 109 | complete -o filenames -F _nsscache nsscache 110 | 111 | # ex: filetype=sh 112 | -------------------------------------------------------------------------------- /nsscache.spec: -------------------------------------------------------------------------------- 1 | Summary: Asynchronously synchronise local NSS databases with remote directory services 2 | Name: nsscache 3 | Version: 0.8.3 4 | Release: 1 5 | License: GPLv2 6 | Group: System Environment/Base 7 | Packager: Oliver Hookins 8 | 9 | URL: http://code.google.com/p/nsscache/ 10 | Source: http://nsscache.googlecode.com/files/%{name}-%{version}.tar.gz 11 | 12 | Requires: python, python-ldap 13 | BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n) 14 | BuildArchitectures: noarch 15 | BuildRequires: python, python-ldap 16 | 17 | %description 18 | nsscache is a Python library and a commandline frontend to that library that 19 | synchronises a local NSS cache against a remote directory service, such as 20 | LDAP. 21 | 22 | %prep 23 | %setup -q 24 | 25 | %build 26 | CFLAGS="%{optflags}" %{__python} setup.py build 27 | 28 | %install 29 | %{__rm} -rf %{buildroot} 30 | %{__python} setup.py install --root="%{buildroot}" --prefix="%{_prefix}" 31 | 32 | %clean 33 | %{__rm} -rf %{buildroot} 34 | 35 | %files 36 | %defattr(-, root, root, 0755) 37 | %config /etc/nsscache.conf 38 | %exclude /usr/bin/runtests.* 39 | /usr/bin/nsscache 40 | /usr/lib/python2.6/site-packages/nss_cache/ 41 | 42 | %changelog 43 | * Tue Jan 06 2009 Oliver Hookins - 0.8.3-1 44 | - Initial packaging 45 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | if [ -z $1 ]; then 4 | CURRENT_VERSION=$(PYTHONPATH=. python3 -c 'import nss_cache; print(nss_cache.__version__)') 5 | a=( ${CURRENT_VERSION//./ } ) 6 | (( a[${#a[@]}-1] += 1 )) 7 | NEW_VERSION=$(IFS=.; echo "${a[*]}") 8 | else 9 | NEW_VERSION=$1 10 | fi 11 | 12 | echo Minting $NEW_VERSION 13 | DATE=$(date +%Y-%m-%d) 14 | 15 | sed -i "1c\.TH NSSCACHE 1 $DATE \"nsscache $NEW_VERSION\" \"User Commands\"" nsscache.1 16 | sed -i "1c\.TH NSSCACHE.CONF 5 $DATE \"nsscache $NEW_VERSION\" \"File formats\"" nsscache.conf.5 17 | sed -i "s/__version__ = '.*'/__version__ = '$NEW_VERSION'/" nss_cache/__init__.py 18 | 19 | 20 | git commit -a -m "Mint version $NEW_VERSION" 21 | git tag -s "version/$NEW_VERSION" -m "version/$NEW_VERSION" 22 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | boto3 3 | google-cloud-storage 4 | pycurl==7.45.6 5 | python3-ldap 6 | python-ldap 7 | packaging 8 | -------------------------------------------------------------------------------- /rpm/postinst.sh: -------------------------------------------------------------------------------- 1 | if [ -f /etc/nsscache.conf.rpmsave ]; then 2 | cp -a /etc/nsscache.conf /etc/nsscache.conf.rpmnew 3 | mv -f /etc/nsscache.conf.rpmsave /etc/nsscache.conf 4 | fi 5 | -------------------------------------------------------------------------------- /rpm/preinst.sh: -------------------------------------------------------------------------------- 1 | if [ -f /etc/nsscache.conf ]; then 2 | mv /etc/nsscache.conf /etc/nsscache.conf.rpmsave 3 | fi 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_rpm] 2 | release = 1 3 | doc_files = COPYING 4 | THANKS 5 | nsscache.cron 6 | requires = python-pycurl 7 | python3-ldap 8 | pre_install = rpm/preinst.sh 9 | post_install = rpm/postinst.sh 10 | 11 | [aliases] 12 | test=pytest 13 | 14 | [yapf] 15 | based_on_style = google 16 | 17 | [pylint] 18 | 19 | [isort] 20 | profile = "black" 21 | 22 | [flake8] 23 | max-line-length = 120 24 | extend-ignore = E203 25 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # Copyright 2007 Google Inc. 4 | # 5 | # This program is free software; you can redistribute it and/or 6 | # modify it under the terms of the GNU General Public License 7 | # as published by the Free Software Foundation; either version 2 8 | # of the License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software Foundation, 17 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 | """Distutils setup for nsscache tool and nss_cache package.""" 19 | 20 | __author__ = "jaq@google.com (Jamie Wilkinson)" 21 | 22 | from setuptools import setup, find_packages 23 | 24 | import nss_cache 25 | 26 | setup( 27 | name="nsscache", 28 | version=nss_cache.__version__, 29 | author="Jamie Wilkinson", 30 | author_email="jaq@google.com", 31 | url="https://github.com/google/nsscache", 32 | description="nsscache tool and library", 33 | license="GPL", 34 | long_description="""nsscache is a Python library and a commandline frontend to that library 35 | that synchronises a local NSS cache against a remote directory service, such 36 | as LDAP.""", 37 | classifiers=[ 38 | "Development Status :: 4 - Beta", 39 | "Environment :: Console", 40 | "Indended Audience :: System Administrators", 41 | "License :: OSI Approved :: GPL", 42 | "Operating System :: POSIX", 43 | "Programming Language :: Python", 44 | "Topic :: System", 45 | ], 46 | packages=[ 47 | "nss_cache", 48 | "nss_cache.caches", 49 | "nss_cache.maps", 50 | "nss_cache.util", 51 | "nss_cache.update", 52 | "nss_cache.sources", 53 | ], 54 | scripts=["nsscache"], 55 | data_files=[("config", ["nsscache.conf"])], 56 | python_requires="~=3.4", 57 | setup_requires=["pytest-runner"], 58 | tests_require=["pytest", "mox3", "pytest-cov", "python-coveralls"], 59 | extras_require={ 60 | "ldap": ["python3-ldap", "python-ldap"], 61 | "http": ["pycurl"], 62 | "s3": ["boto3"], 63 | "consul": ["pycurl"], 64 | "gcs": ["google-cloud-storage"], 65 | }, 66 | ) 67 | -------------------------------------------------------------------------------- /tests/default.ldif: -------------------------------------------------------------------------------- 1 | dn: dc=example,dc=com 2 | dc: example 3 | objectClass: dcObject 4 | objectClass: organization 5 | o: Example, Inc. 6 | 7 | dn: ou=people,dc=example,dc=com 8 | objectclass: top 9 | objectclass: organizationalUnit 10 | ou: people 11 | 12 | dn: ou=group,dc=example,dc=com 13 | objectclass: top 14 | objectclass: organizationalUnit 15 | ou: groups 16 | 17 | dn: uid=jaq,ou=people,dc=example,dc=com 18 | objectClass: top 19 | objectClass: account 20 | objectClass: posixAccount 21 | objectClass: shadowAccount 22 | cn: Jamie Wilkinson 23 | uid: jaq 24 | userPassword: {CRYPT}e1y7ep455\//0rD 25 | homeDirectory: /home/jaq 26 | uidNumber: 37 27 | gidNumber: 31337 28 | loginShell: /bin/zsh 29 | shadowLastChange: 0 30 | shadowMax: 0 31 | shadowWarning: 0 32 | 33 | dn: cn=hax0rs,ou=group,dc=example,dc=com 34 | objectClass: posixGroup 35 | cn: hax0rs 36 | gidNumber: 31337 37 | memberUid: jaq 38 | -------------------------------------------------------------------------------- /tests/nsscache.conf: -------------------------------------------------------------------------------- 1 | # Example /etc/nsscache.conf - configuration for nsscache 2 | # 3 | # nsscache loads a config file from the environment variable NSSCACHE_CONFIG 4 | # 5 | # By default this is /etc/nsscache.conf 6 | # 7 | # Commented values are overrideable defaults, uncommented values 8 | # require you to set them. 9 | 10 | [DEFAULT] 11 | 12 | # Default NSS data source module name 13 | source = ldap 14 | 15 | # Default NSS data cache module name; 'files' is compatible with the 16 | # libnss-cache NSS module. 17 | cache = files 18 | 19 | # NSS maps to be cached 20 | maps = passwd, group, shadow 21 | 22 | # Directory to store our update/modify timestamps 23 | timestamp_dir = /var/lib/nsscache 24 | 25 | # Lockfile to use for update/repair operations 26 | lockfile = /var/run/nsscache 27 | 28 | # Defaults for specific modules; prefaced with "modulename_" 29 | 30 | ## 31 | # ldap module defaults. 32 | # 33 | 34 | # Enable to connect to Active Directory. 35 | # Leave disabled if connecting to openldap or slapd 36 | ldap_ad = 1 37 | 38 | # LDAP URI to query for NSS data 39 | ldap_uri = ldaps://local.domain 40 | 41 | # Default LDAP search scope 42 | ldap_scope = sub 43 | 44 | # Default LDAP BIND DN, empty string is an anonymous bind 45 | ldap_bind_dn = administrator@local.domain 46 | 47 | # Default LDAP password, empty DN and empty password is used for 48 | # anonymous binds 49 | ldap_bind_password = 4dm1n_s3cr36_v3ry_c0mpl3x 50 | 51 | # Default setting for requiring tls certificates, one of: 52 | # never, hard, demand, allow, try 53 | ldap_tls_require_cert = 'never' 54 | 55 | # Default directoy for trusted CAs 56 | ldap_tls_cacertdir = '/etc/ssl/certs/' 57 | 58 | # Default filename for trusted CAs 59 | ldap_tls_cacertfile = '/etc/ssl/certs/ad.pem' 60 | 61 | # Replace all users' shells with the specified one. 62 | ldap_override_shell = '/bin/bash' 63 | 64 | # Set directory for all users in passwd under /home. 65 | ldap_home_dir = 1 66 | 67 | # Debug logging 68 | ldap_debug = 3 69 | 70 | ## 71 | # files module defaults 72 | 73 | # Directory to store the plain text files 74 | files_dir = /etc 75 | 76 | # Suffix used on the files module database files 77 | files_cache_filename_suffix = cache 78 | 79 | ### 80 | # Optional per-map sections, if present they will override the above 81 | # defaults. The examples below show you some common values to override 82 | # 83 | 84 | [passwd] 85 | ldap_base = DC=local,DC=domain 86 | ldap_filter = (&(objectCategory=User)(memberOf=CN=Admins,CN=Users,DC=local,DC=domain)) 87 | 88 | [group] 89 | ldap_base = DC=local,DC=domain 90 | ldap_filter = (|(&(objectCategory=Group)(CN=Admins))(&(objectCategory=User)(memberOf=CN=Admins,CN=Users,DC=local,DC=domain))) 91 | 92 | [shadow] 93 | ldap_base = DC=local,DC=domain 94 | ldap_filter = (&(objectCategory=User)(memberOf=CN=Admins,CN=Users,DC=local,DC=domain)) 95 | 96 | [suffix] 97 | prefix = "" 98 | suffix = "" 99 | -------------------------------------------------------------------------------- /tests/samba.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eux 2 | 3 | export DEBIAN_FRONTEND=noninteractive 4 | 5 | apt-get update 6 | 7 | PACKAGES=( 8 | 'samba' 9 | 'samba-dsdb-modules' 10 | 'samba-vfs-modules' 11 | 'winbind' 12 | 'heimdal-clients' 13 | ) 14 | 15 | # Install needed packages 16 | for package in "${PACKAGES[@]}"; do 17 | apt-get -y install "$package" 18 | done 19 | 20 | # Samba must not be running during the provisioning 21 | service smbd stop 22 | service nmbd stop 23 | service winbind stop 24 | service samba-ad-dc stop 25 | 26 | # Domain provision 27 | rm -fr /etc/samba/smb.conf 28 | /usr/bin/samba-tool domain provision --realm=LOCAL.DOMAIN --domain=LOCAL --server-role=dc --dns-backend=SAMBA_INTERNAL --adminpass='4dm1n_s3cr36_v3ry_c0mpl3x' --use-rfc2307 -d 1 29 | 30 | # Start samba-ad-dc service only 31 | rm -fr /etc/systemd/system/samba-ad-dc.service 32 | service samba-ad-dc start 33 | 34 | # Add users and groups 35 | /usr/bin/samba-tool user create user1 --use-username-as-cn --surname=Test1 --given-name=User1 --random-password 36 | /usr/bin/samba-tool user create user2 --use-username-as-cn --surname=Test2 --given-name=User2 --random-password 37 | /usr/bin/samba-tool user create user3 --use-username-as-cn --surname=Test3 --given-name=User3 --random-password 38 | /usr/bin/samba-tool user create user4 --use-username-as-cn --surname=Test4 --given-name=User4 --random-password 39 | /usr/bin/samba-tool user create user5 --use-username-as-cn --surname=Test5 --given-name=User5 --random-password 40 | 41 | # Add some groups 42 | /usr/bin/samba-tool group add IT 43 | /usr/bin/samba-tool group add Admins 44 | /usr/bin/samba-tool group add Devs 45 | /usr/bin/samba-tool group add DevOps 46 | 47 | # Create members 48 | /usr/bin/samba-tool group addmembers IT Admins,Devs,DevOps,user1 49 | /usr/bin/samba-tool group addmembers Admins user2,user3 50 | /usr/bin/samba-tool group addmembers Devs user4 51 | /usr/bin/samba-tool group addmembers DevOps user5 52 | 53 | # Add AD certificate 54 | echo -n | openssl s_client -connect localhost:636 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > /usr/local/share/ca-certificates/ad.crt 55 | update-ca-certificates 56 | 57 | # Add cache to nsswitch 58 | cat > '/etc/nsswitch.conf' << EOF 59 | passwd: files cache 60 | group: files cache 61 | shadow: files cache 62 | gshadow: files 63 | 64 | hosts: files dns 65 | networks: files 66 | 67 | protocols: db files 68 | services: db files 69 | ethers: db files 70 | rpc: db files 71 | 72 | netgroup: nis 73 | EOF 74 | -------------------------------------------------------------------------------- /tests/slapd-nsscache.conf.tmpl: -------------------------------------------------------------------------------- 1 | # $Id: //depot/ops/src/nsscache/nsscache.conf.ldap#4 $ 2 | # 3 | # See /usr/share/doc/nsscache/examples/nsscache.conf for 4 | # detailed information about configuration file formats, defaults, 5 | # and options. 6 | 7 | [DEFAULT] 8 | 9 | source = @source@ 10 | cache = @cache@ 11 | maps = passwd, group, shadow 12 | lockfile = @workdir@/lock 13 | 14 | ldap_uri = ldapi://@workdir@/ldapi 15 | 16 | ldap_base = ou=people,dc=example,dc=com 17 | ldap_filter = (objectclass=posixAccount) 18 | 19 | files_cache_filename_suffix = cache 20 | 21 | files_dir = @workdir@/files 22 | 23 | timestamp_dir = @workdir@/ldap-timestamps-@cache@ 24 | 25 | [group] 26 | ldap_base = ou=group,dc=example,dc=com 27 | ldap_filter = (objectclass=posixGroup) 28 | 29 | [shadow] 30 | ldap_filter = (objectclass=shadowAccount) 31 | -------------------------------------------------------------------------------- /tests/slapd-regtest: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -x 4 | 5 | SLAPADD=/usr/sbin/slapadd 6 | SLAPD=/usr/sbin/slapd 7 | 8 | if [[ -z ${WORKDIR-} ]]; then 9 | WORKDIR=$(mktemp -d -t nsscache.regtest.XXXXXX) 10 | ARTIFACTS=${WORKDIR} 11 | fi 12 | 13 | slapd_apparmor_bkp="${WORKDIR}/slapd_profile.bkp" 14 | slapd_apparmor_override="/etc/apparmor.d/local/usr.sbin.slapd" 15 | slapd_apparmor="/etc/apparmor.d/usr.sbin.slapd" 16 | 17 | cleanup() { 18 | if [[ -f "$slapd_apparmor_bkp" ]]; then 19 | sudo mv "$slapd_apparmor_bkp" "$slapd_apparmor_override" 20 | sudo apparmor_parser -r -T -W "$slapd_apparmor" 21 | fi 22 | if [[ -e "$WORKDIR/slapd.pid" ]]; then 23 | kill -TERM $(cat $WORKDIR/slapd.pid) 24 | fi 25 | if [[ -z ${ADTTMP-} ]]; then 26 | rm -rf $WORKDIR 27 | fi 28 | } 29 | 30 | trap cleanup 0 INT QUIT ABRT PIPE TERM 31 | 32 | TESTDIR=$(dirname -- "$0") 33 | 34 | apparmor_enabled() { 35 | if [ -x /usr/sbin/aa-status ]; then 36 | sudo /usr/sbin/aa-status --enabled && apparmor_enabled="0" || apparmor_enabled="1" 37 | else 38 | apparmor_enabled="1" 39 | fi 40 | return "$apparmor_enabled" 41 | } 42 | 43 | override_apparmor() { 44 | # backup existing override 45 | cp -af "$slapd_apparmor_override" "$slapd_apparmor_bkp" 46 | 47 | # the test suite brings up a test slapd server running 48 | # off /tmp/. 49 | echo "${WORKDIR}/ rw," | sudo tee "$slapd_apparmor_override" 50 | echo "${WORKDIR}/** rwk," | sudo tee -a "$slapd_apparmor_override" 51 | echo "${ARTIFACTS}/ rw," | sudo tee -a "$slapd_apparmor_override" 52 | echo "${ARTIFACTS}/** rwk," | sudo tee -a "$slapd_apparmor_override" 53 | sudo apparmor_parser -r -T -W "$slapd_apparmor" 54 | } 55 | 56 | setup_slapd() { 57 | set -e 58 | mkdir -p $WORKDIR/ldap 59 | sed -e "s!@workdir@!$WORKDIR!" \ 60 | < ${TESTDIR}/slapd.conf.tmpl > $ARTIFACTS/slapd.conf 61 | $SLAPD -VVV || true 62 | $SLAPADD -d -1 -f $ARTIFACTS/slapd.conf -b dc=example,dc=com -l ${TESTDIR}/default.ldif 63 | $SLAPD -h ldapi://${WORKDIR//\//%2F}%2Fldapi -f $ARTIFACTS/slapd.conf & 64 | slappid=$! 65 | attempts=0 66 | until ldapsearch -x -H ldapi://${WORKDIR//\//%2F}%2Fldapi -b "dc=example,dc=com" '(objectclass=*)'; do 67 | attempts=$(($attempts + 1)) 68 | if [[ $attempts -gt 10 ]]; then 69 | echo "failed to connect to slapd in 60 attempts" 70 | exit 1 71 | fi 72 | sleep 0.1 73 | done 74 | set +e 75 | } 76 | 77 | run_nsscache() { 78 | source=$1 79 | cache=$2 80 | config_orig="${TESTDIR}/slapd-nsscache.conf.tmpl" 81 | config=$(mktemp -p ${ARTIFACTS} nsscache.${source}.conf.XXXXXX) 82 | sed -e "s!@cache@!$cache!" \ 83 | -e "s!@source@!$source!" \ 84 | -e "s!@workdir@!$WORKDIR!" \ 85 | < $config_orig > $config 86 | mkdir $WORKDIR/$cache 87 | mkdir $WORKDIR/ldap-timestamps-$cache 88 | 89 | nsscache status 90 | 91 | nsscache -d -c "${config}" update --full 92 | r=$? 93 | if [[ $r -ne 0 ]]; then 94 | echo FAILED: $r 95 | fi 96 | test_${cache} 97 | 98 | nsscache -d -c "${config}" status 99 | } 100 | 101 | test_files() { 102 | ls -alR $WORKDIR 103 | set -e 104 | grep jaq $WORKDIR/files/passwd.cache 105 | grep jaq $WORKDIR/files/passwd.cache.ixname 106 | grep 37 $WORKDIR/files/passwd.cache.ixuid 107 | grep hax0rs $WORKDIR/files/group.cache 108 | grep hax0rs $WORKDIR/files/group.cache.ixname 109 | grep 31337 $WORKDIR/files/group.cache.ixgid 110 | grep jaq $WORKDIR/files/shadow.cache 111 | grep jaq $WORKDIR/files/shadow.cache.ixname 112 | [[ $(stat -c%A $WORKDIR/files/shadow.cache) == "-rw-r-----" ]] || exit 1 113 | [[ $(stat -c%A $WORKDIR/files/shadow.cache.ixname) == "-rw-r-----" ]] || exit 1 114 | } 115 | 116 | check () { 117 | which nsscache 118 | if [[ $? -ne 0 ]]; then 119 | ( 120 | cd ${TESTDIR}/.. 121 | pip3 install --target="${WORKDIR}" . 122 | ) 123 | export PATH=$PATH:${WORKDIR}/bin 124 | fi 125 | set -e 126 | nsscache --version 127 | set +e 128 | } 129 | 130 | check 131 | if apparmor_enabled; then 132 | override_apparmor 133 | fi 134 | setup_slapd 135 | run_nsscache ldap files 136 | 137 | echo OK 138 | -------------------------------------------------------------------------------- /tests/slapd.conf.tmpl: -------------------------------------------------------------------------------- 1 | include /etc/ldap/schema/core.schema 2 | include /etc/ldap/schema/cosine.schema 3 | include /etc/ldap/schema/nis.schema 4 | 5 | loglevel -1 6 | pidfile @workdir@/slapd.pid 7 | 8 | 9 | moduleload back_mdb.la 10 | 11 | database mdb 12 | suffix "dc=example,dc=com" 13 | directory @workdir@/ldap 14 | --------------------------------------------------------------------------------