├── pyproject.toml
├── .gitignore
├── Makefile
├── CONTRIBUTORS.md
├── .github
├── renovate.json
└── workflows
│ ├── label-when-deployed.yml
│ └── main.yml
├── eslint.config.cjs
├── tox.ini
├── create-agreement.py
├── ipaserver
└── plugins
│ ├── fasutils.py
│ ├── baseruserfas.py
│ ├── stageuserfas.py
│ ├── userfas.py
│ ├── groupfas.py
│ └── fasagreement.py
├── install.sh
├── .packit.yaml
├── ui
└── js
│ └── plugins
│ ├── userfas
│ └── userfas.js
│ ├── groupfas
│ └── groupfas.js
│ └── fasagreement
│ └── fasagreement.js
├── schema.d
└── 89-fasschema.ldif
├── updates
└── 89-fas.update
├── README.md
└── COPYING
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.black]
2 | line-length = 78
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__
2 | *.pyc
3 | *.pyo
4 | *.tar.gz
5 | *.rpm
6 | ~*
7 |
8 | .tox
9 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | BLACK=black
2 |
3 | all: lint
4 |
5 | lint:
6 | $(BLACK) --check .
7 |
8 | black:
9 | $(BLACK) .
10 |
--------------------------------------------------------------------------------
/CONTRIBUTORS.md:
--------------------------------------------------------------------------------
1 | * Aurélien Bompard
2 | * Christian Heimes
3 | * Michael Scherer
4 | * Rick Elrod
5 | * Ryan Lerch
6 | * Stephen Coady
--------------------------------------------------------------------------------
/.github/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": ["local>fedora-infra/shared:renovate-config"]
4 | }
5 |
--------------------------------------------------------------------------------
/.github/workflows/label-when-deployed.yml:
--------------------------------------------------------------------------------
1 | name: Apply labels when deployed
2 |
3 | on:
4 | push:
5 | branches:
6 | - staging
7 | - stable
8 | - main
9 | - develop
10 |
11 | jobs:
12 | label:
13 | name: Apply labels
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - name: Staging deployment
18 | uses: fedora-infra/label-when-in-branch@v1
19 | with:
20 | token: ${{ secrets.GITHUB_TOKEN }}
21 | branch: staging
22 | label: deployed:staging
23 | - name: Production deployment
24 | uses: fedora-infra/label-when-in-branch@v1
25 | with:
26 | token: ${{ secrets.GITHUB_TOKEN }}
27 | branch: stable
28 | label: deployed:prod
29 |
--------------------------------------------------------------------------------
/eslint.config.cjs:
--------------------------------------------------------------------------------
1 | const globals = require("globals");
2 | const js = require("@eslint/js");
3 |
4 | const {
5 | FlatCompat,
6 | } = require("@eslint/eslintrc");
7 |
8 | const compat = new FlatCompat({
9 | baseDirectory: __dirname,
10 | recommendedConfig: js.configs.recommended,
11 | allConfig: js.configs.all
12 | });
13 |
14 | module.exports = [...compat.extends("eslint:recommended"), {
15 | languageOptions: {
16 | globals: {
17 | ...globals.browser,
18 | ...globals.amd,
19 | Atomics: "readonly",
20 | SharedArrayBuffer: "readonly",
21 | },
22 |
23 | ecmaVersion: 11,
24 | sourceType: "script",
25 | },
26 |
27 | rules: {
28 | "comma-dangle": ["error", "never"],
29 | },
30 | }];
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist = lint,format,bandit,jslint
3 | #isolated_build = true
4 |
5 | [testenv]
6 | passenv = HOME
7 | deps =
8 | ipalib
9 | skip_install = true
10 |
11 | [testenv:lint]
12 | deps = flake8
13 | commands =
14 | flake8 {posargs}
15 |
16 | [testenv:format]
17 | deps = black
18 | commands =
19 | black --check --diff {posargs:.}
20 |
21 | [testenv:bandit]
22 | deps = bandit
23 | commands =
24 | bandit -r ipaserver/ -ll
25 |
26 | [testenv:jslint]
27 | deps =
28 | skipsdist = true
29 | set_env =
30 | NPM_CONFIG_PREFIX={envdir}
31 | NODE_PATH={envdir}/lib/node_modules
32 | allowlist_externals =
33 | npm
34 | commands =
35 | npm install -g globals @eslint/js @eslint/eslintrc eslint@latest
36 | eslint {toxinidir}/ui/js/
37 |
38 | [flake8]
39 | show-source = True
40 | max-line-length = 100
41 | ignore = E203,W503
42 | exclude = .git,.tox,dist,*egg
43 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Test & Build
2 |
3 | on:
4 | push:
5 | branches:
6 | - stable
7 | - develop
8 | - main
9 | tags:
10 | - v**
11 | pull_request:
12 | branches:
13 | - stable
14 | - develop
15 | - main
16 |
17 | jobs:
18 |
19 | checks:
20 | name: Checks
21 | runs-on: ubuntu-latest
22 | container: fedorapython/fedora-python-tox:latest
23 | steps:
24 | - uses: actions/checkout@v4
25 |
26 | - name: Install dependencies
27 | run: |
28 | dnf install -y nodejs-npm
29 |
30 | - name: Mark the working directory as safe for Git
31 | run: git config --global --add safe.directory $PWD
32 |
33 | - name: Run the tests
34 | run: tox -e ${{ matrix.toxenv }}
35 |
36 | strategy:
37 | matrix:
38 | toxenv:
39 | - lint
40 | - format
41 | - bandit
42 | - jslint
43 |
44 |
45 | github-release:
46 | name: Create a GitHub Release 📢
47 | if: startsWith(github.ref, 'refs/tags/') && !contains(github.ref, 'rc') # only release on final tag pushes
48 | needs:
49 | - checks
50 | runs-on: ubuntu-latest
51 | permissions:
52 | contents: write # IMPORTANT: mandatory for making GitHub Releases
53 | id-token: write # IMPORTANT: mandatory for sigstore
54 |
55 | steps:
56 |
57 | - name: Release
58 | uses: softprops/action-gh-release@v2
59 | with:
60 | generate_release_notes: true
61 |
--------------------------------------------------------------------------------
/create-agreement.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | from argparse import ArgumentParser
4 | from subprocess import run
5 |
6 |
7 | def run_cmd(cmd, **kwargs):
8 | print(" ".join(cmd))
9 | run(cmd, check=True, **kwargs)
10 |
11 |
12 | def parse_args():
13 | parser = ArgumentParser()
14 | parser.add_argument("name", metavar="NAME", help="agreement name")
15 | parser.add_argument("--desc", help="agreement text")
16 | return parser.parse_args()
17 |
18 |
19 | def main():
20 | args = parse_args()
21 | agreement_name = args.name
22 | cmd = ["ipa", "fasagreement-add", agreement_name]
23 | if args.desc:
24 | cmd.extend(["--desc", args.desc])
25 | # Create the agreement
26 | run_cmd(cmd)
27 | # Create the corresponding group
28 | group_name = f"signed_{agreement_name}"
29 | run_cmd(
30 | [
31 | "ipa",
32 | "group-add",
33 | group_name,
34 | "--desc",
35 | f"Signers of the {agreement_name}",
36 | ]
37 | )
38 | # Add the automember rule
39 | run_cmd(["ipa", "automember-add", "--type", "group", group_name])
40 | run_cmd(
41 | [
42 | "ipa",
43 | "automember-add-condition",
44 | "--type=group",
45 | "--key=memberof",
46 | "--inclusive-regex",
47 | f"^cn={agreement_name},cn=fasagreements,",
48 | group_name,
49 | ]
50 | )
51 |
52 |
53 | if __name__ == "__main__":
54 | main()
55 |
--------------------------------------------------------------------------------
/ipaserver/plugins/fasutils.py:
--------------------------------------------------------------------------------
1 | from urllib.parse import urlparse
2 |
3 | from ipalib.parameters import Str
4 | from ipapython.ipavalidate import Email as valid_email
5 |
6 |
7 | class URL(Str):
8 | kwargs = Str.kwargs + (
9 | ("url_schemes", frozenset, frozenset({"http", "https"})),
10 | )
11 |
12 | def _rule_url_schemes(self, _, value):
13 | try:
14 | url = urlparse(value)
15 | except Exception as e:
16 | return _("cannot parse url '{url}', {error}").format(
17 | url=value, error=str(e)
18 | )
19 | if url.scheme not in self.url_schemes:
20 | return _(
21 | "unsupported scheme, url must start with: {scheme}"
22 | ).format(scheme=", ".join(sorted(self.url_schemes)))
23 | if not url.netloc:
24 | return _("empty host name")
25 | return None
26 |
27 |
28 | class Email(Str):
29 | kwargs = Str.kwargs + (("email", bool, True),)
30 |
31 | def _rule_email(self, _, value):
32 | if not valid_email(value):
33 | return _("Invalid email address")
34 | return None
35 |
36 |
37 | class IRCChannel(Str):
38 | kwargs = Str.kwargs + (("ircurl", bool, True),)
39 |
40 | def _rule_ircurl(self, _, value):
41 | return None
42 |
43 | def _convert_scalar(self, value, index=None):
44 | value = super()._convert_scalar(value, index)
45 | if ":/" not in value:
46 | # Default to IRC
47 | value = value.lstrip("#")
48 | value = "irc:///{}".format(value)
49 | return value
50 |
--------------------------------------------------------------------------------
/install.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -e
3 |
4 | SITE_PACKAGES=$(python3 -c 'from sys import version_info as v; print(f"/usr/lib/python{v.major}.{v.minor}/site-packages")')
5 |
6 | if [ "$1" == "--copy-only" ]; then
7 | COPYONLY=true
8 | fi
9 |
10 | if [ -f /usr/share/ipa/schema.d/89-fasschema.ldif -a -f /usr/share/ipa/updates/89-fas.update ]; then
11 | NEEDS_UPGRADE=0;
12 | else
13 | NEEDS_UPGRADE=1;
14 | fi
15 |
16 | cp schema.d/89-fasschema.ldif /usr/share/ipa/schema.d/
17 | rm -f /usr/share/ipa/schema.d/99-fasschema.ldif
18 |
19 | cp updates/89-fas.update /usr/share/ipa/updates/
20 | rm -f /usr/share/ipa/updates/99-fas.update
21 |
22 | mkdir -p -m 755 /usr/share/ipa/ui/js/plugins/userfas
23 | cp ui/js/plugins/userfas/userfas.js /usr/share/ipa/ui/js/plugins/userfas/
24 | mkdir -p -m 755 /usr/share/ipa/ui/js/plugins/groupfas
25 | cp ui/js/plugins/groupfas/groupfas.js /usr/share/ipa/ui/js/plugins/groupfas/
26 | mkdir -p -m 755 /usr/share/ipa/ui/js/plugins/fasagreement
27 | cp ui/js/plugins/fasagreement/fasagreement.js \
28 | /usr/share/ipa/ui/js/plugins/fasagreement/
29 |
30 | cp ipaserver/plugins/*.py ${SITE_PACKAGES}/ipaserver/plugins/
31 | python3 -m compileall ${SITE_PACKAGES}/ipaserver/plugins/
32 |
33 | mkdir -p /usr/local/bin
34 | install -p -m 755 create-agreement.py /usr/local/bin/ipa-create-agreement
35 |
36 | if [ -z "$COPYONLY" ]; then
37 | if [ $NEEDS_UPGRADE = 1 ]; then
38 | ipa-server-upgrade
39 | else
40 | ipa-ldap-updater \
41 | -S /usr/share/ipa/schema.d/89-fasschema.ldif \
42 | /usr/share/ipa/updates/89-fas.update
43 | systemctl restart httpd.service
44 | fi
45 | fi
46 |
47 | echo "NOTE: $0 is a hack for internal development."
48 | echo "Some changes require a proper ipa-server-upgrade or ipactl restart."
49 |
--------------------------------------------------------------------------------
/.packit.yaml:
--------------------------------------------------------------------------------
1 | # See the documentation for more information:
2 | # https://packit.dev/docs/configuration/
3 |
4 | specfile_path: freeipa-fas.spec
5 |
6 | # add or remove files that should be synced
7 | files_to_sync:
8 | - freeipa-fas.spec
9 | - .packit.yaml
10 | - sources
11 |
12 | # name in upstream package repository or registry (e.g. in PyPI)
13 | upstream_package_name: freeipa-fas
14 | # downstream (Fedora) RPM package name
15 | downstream_package_name: freeipa-fas
16 | upstream_tag_template: v{version}
17 |
18 | srpm_build_deps:
19 | - curl
20 |
21 | actions:
22 | post-upstream-clone:
23 | # fetch specfile, patches, documentation etc. from the downstream
24 | - "curl https://src.fedoraproject.org/rpms/freeipa-fas/raw/rawhide/f/freeipa-fas.spec -o freeipa-fas.spec"
25 | - "curl https://src.fedoraproject.org/rpms/freeipa-fas/raw/main/f/sources -o sources"
26 | create-archive:
27 | - "sh -c 'ver=$(git tag | tail -1 | sed s/v//); git archive --prefix freeipa-fas-${ver}/ HEAD -o freeipa-fas-${ver}.tar.gz'"
28 | - "sh -c 'echo freeipa-fas-$(git tag | tail -1 | sed s/v//).tar.gz'"
29 |
30 | jobs:
31 | # upon upstream PRs, test builds
32 | - job: tests
33 | trigger: pull_request
34 | metadata:
35 | targets:
36 | - fedora-latest-stable
37 | - fedora-development
38 | - epel-9
39 |
40 | # upon upstream releases, perform COPR builds
41 | - job: copr_build
42 | trigger: pull_request
43 | metadata:
44 | targets:
45 | - fedora-latest-stable
46 | - fedora-development
47 | - epel-9
48 |
49 | # upon upstream releases, perform COPR builds
50 | - job: copr_build
51 | trigger: release
52 | metadata:
53 | targets:
54 | - fedora-latest-stable
55 | - fedora-development
56 | - epel-9
57 |
58 | # upon downstream changes, create a PR upstream with sync'd files from above
59 | - job: sync_from_downstream
60 | trigger: commit
61 |
62 | # land upstream release in fedora dist-git - no builds
63 | - job: propose_downstream
64 | trigger: release
65 | metadata:
66 | dist_git_branches:
67 | - fedora-latest-stable
68 | - fedora-development
69 | - epel-9
70 |
71 | # create an srpm from upstream and submit a scratch build to koji
72 | - job: upstream_koji_build
73 | trigger: release
74 | metadata:
75 | targets:
76 | - fedora-latest-stable
77 | - fedora-development
78 | - epel-9
79 |
80 | # downstream automation
81 |
82 | # trigger a build in koji for a new dist-git commit
83 | - job: koji_build
84 | trigger: commit
85 | metadata:
86 | dist_git_branches:
87 | - fedora-latest-stable
88 | - fedora-development
89 | - epel-9
90 |
91 | # create a new update in bodhi for a successful koji build. directly related to `koji_build`
92 | - job: bodhi_update
93 | trigger: commit
94 | metadata:
95 | dist_git_branches:
96 | - fedora-latest-stable
97 | - fedora-development
98 | - epel-9
99 |
--------------------------------------------------------------------------------
/ipaserver/plugins/baseruserfas.py:
--------------------------------------------------------------------------------
1 | #
2 | # FreeIPA plugin for Fedora Account System
3 | # Copyright (C) 2020 FreeIPA FAS Contributors
4 | # See COPYING for license
5 | #
6 | """FreeIPA plugin for Fedora Account System
7 |
8 | Common user extensions
9 | """
10 | from ipalib import _
11 | from ipalib.parameters import DateTime, Str, Bool
12 |
13 | from ipaserver.plugins.baseuser import baseuser
14 | from ipaserver.plugins.internal import i18n_messages
15 |
16 | from .fasutils import URL
17 |
18 | # possible object classes and default attributes are shared between all
19 | # users plugins.
20 | if "fasuser" not in baseuser.possible_objectclasses:
21 | baseuser.possible_objectclasses.append("fasuser")
22 |
23 | fas_user_attributes = [
24 | "fastimezone",
25 | "faslocale",
26 | "fasircnick",
27 | "fasgpgkeyid",
28 | "fasstatusnote",
29 | "fascreationtime",
30 | "fasrhbzemail",
31 | "fasgithubusername",
32 | "fasgitlabusername",
33 | "faswebsiteurl",
34 | "fasisprivate",
35 | "faspronoun",
36 | "fasrssurl",
37 | ]
38 | baseuser.default_attributes.extend(fas_user_attributes)
39 |
40 | baseuser.attribute_members["memberof"].append("fasagreement")
41 |
42 | takes_params = (
43 | Str(
44 | "fastimezone?",
45 | cli_name="fastimezone",
46 | label=_("user timezone"),
47 | maxlength=64,
48 | ),
49 | Str(
50 | "faslocale?",
51 | cli_name="faslocale",
52 | label=_("user locale"),
53 | maxlength=64,
54 | ),
55 | Str(
56 | "fasircnick*",
57 | cli_name="fasircnick",
58 | label=_("IRC nick name"),
59 | maxlength=64,
60 | ),
61 | Str(
62 | "fasgpgkeyid*",
63 | cli_name="fasgpgkeyid",
64 | label=_("GPG Key ids"),
65 | minlength=16,
66 | maxlength=40,
67 | ),
68 | Str(
69 | "fasstatusnote?",
70 | cli_name="fasstatusnote",
71 | label=_("User status note"),
72 | ),
73 | DateTime(
74 | "fascreationtime?",
75 | cli_name="fascreationtime",
76 | label=_("user creation time"),
77 | ),
78 | Str(
79 | "fasrhbzemail?",
80 | cli_name="fasrhbzemail",
81 | label=_("Red Hat bugzilla email"),
82 | maxlength=255,
83 | normalizer=lambda value: value.strip(),
84 | ),
85 | Str(
86 | "fasgithubusername?",
87 | cli_name="fasgithubusername",
88 | label=_("GitHub username"),
89 | maxlength=255,
90 | normalizer=lambda value: value.strip(),
91 | ),
92 | Str(
93 | "fasgitlabusername?",
94 | cli_name="fasgitlabusername",
95 | label=_("GitLab username"),
96 | maxlength=255,
97 | normalizer=lambda value: value.strip(),
98 | ),
99 | URL(
100 | "faswebsiteurl*",
101 | cli_name="faswebsiteurl",
102 | label=_("Blog URL"),
103 | maxlength=255,
104 | normalizer=lambda value: value.strip(),
105 | ),
106 | Bool(
107 | "fasisprivate?",
108 | cli_name="fasisprivate",
109 | label=_("Hide personal data"),
110 | doc=_("Hide personal data from other users"),
111 | ),
112 | Str(
113 | "faspronoun*",
114 | cli_name="faspronoun",
115 | label=_("Preferred pronouns"),
116 | maxlength=64,
117 | ),
118 | URL(
119 | "fasrssurl*",
120 | cli_name="fasrssurl",
121 | label=_("RSS URL"),
122 | maxlength=255,
123 | normalizer=lambda value: value.strip(),
124 | ),
125 | )
126 |
127 | i18n_messages.messages["userfas"] = {"name": _("Fedora Account System")}
128 |
--------------------------------------------------------------------------------
/ui/js/plugins/userfas/userfas.js:
--------------------------------------------------------------------------------
1 | //
2 | // FreeIPA plugin for Fedora Account System
3 | // Copyright (C) 2019 FreeIPA FAS Contributors
4 | // See COPYING for license
5 | //
6 |
7 | define([
8 | 'freeipa/phases',
9 | 'freeipa/ipa'
10 | ],
11 | function(phases, IPA) {
12 |
13 | // helper function
14 | function get_item(array, attr, value) {
15 |
16 | for (var i = 0, l = array.length; i < l; i++) {
17 | if (array[i][attr] === value) return array[i];
18 | }
19 | return null;
20 | }
21 |
22 | var userfas_plugin = {};
23 |
24 | userfas_plugin.add_user_fas_pre_op = function() {
25 | var section = {
26 | name: 'userfas',
27 | label: '@i18n:userfas.name',
28 | fields: [{
29 | name: 'fastimezone',
30 | flags: ['w_if_no_aci']
31 | }, {
32 | name: 'faslocale',
33 | flags: ['w_if_no_aci']
34 | }, {
35 | $type: 'multivalued',
36 | name: 'fasircnick',
37 | flags: ['w_if_no_aci']
38 | }, {
39 | name: 'fastimezone',
40 | flags: ['w_if_no_aci']
41 | }, {
42 | name: 'faswebsiteurl',
43 | $type: 'multivalued',
44 | flags: ['w_if_no_aci']
45 | }, {
46 | name: 'fasrhbzemail',
47 | flags: ['w_if_no_aci']
48 | }, {
49 | name: 'fasgithubusername',
50 | flags: ['w_if_no_aci']
51 | }, {
52 | name: 'fasgitlabusername',
53 | flags: ['w_if_no_aci']
54 | }, {
55 | $type: 'multivalued',
56 | name: 'fasgpgkeyid',
57 | flags: ['w_if_no_aci']
58 | }, {
59 | name: 'fasisprivate',
60 | $type: 'checkbox',
61 | flags: ['w_if_no_aci']
62 | }, {
63 | $type: 'datetime',
64 | name: 'fascreationtime',
65 | read_only: true
66 | }, {
67 | name: 'fasstatusnote',
68 | flags: ['w_if_no_aci']
69 | }, {
70 | name: 'faspronoun',
71 | $type: 'multivalued',
72 | flags: ['w_if_no_aci']
73 | }, {
74 | name: 'fasrssurl',
75 | $type: 'multivalued',
76 | flags: ['w_if_no_aci']
77 | }]
78 | };
79 | var fasagreement = {
80 | $type: 'association',
81 | $pre_ops: [IPA.user.association_facet_ss_pre_op],
82 | name: 'memberof_fasagreement',
83 | associator: IPA.serial_associator,
84 | add_method: 'add_user',
85 | add_title: '@i18n:fasagreement.add',
86 | remove_method: 'remove_user',
87 | remove_title: '@i18n:fasagreement.remove'
88 | };
89 | [IPA.user.entity_spec, IPA.stageuser.stageuser_spec].forEach(function(spec) {
90 | var facet = get_item(spec.facets, '$type', 'details');
91 | facet.sections.push(section);
92 |
93 | spec.facets.push(fasagreement);
94 | });
95 | return true;
96 | };
97 |
98 | phases.on('customization', userfas_plugin.add_user_fas_pre_op);
99 |
100 | return userfas_plugin;
101 | });
102 |
--------------------------------------------------------------------------------
/ipaserver/plugins/stageuserfas.py:
--------------------------------------------------------------------------------
1 | #
2 | # FreeIPA plugin for Fedora Account System
3 | # Copyright (C) 2020 FreeIPA FAS Contributors
4 | # See COPYING for license
5 | #
6 | """FreeIPA plugin for Fedora Account System
7 |
8 | Stage user extension
9 | """
10 | from ipalib import _
11 | from ipalib import errors
12 |
13 | from ipaserver.plugins.stageuser import stageuser
14 | from ipaserver.plugins.stageuser import stageuser_add
15 | from ipaserver.plugins.stageuser import stageuser_mod
16 |
17 | from .baseruserfas import takes_params
18 | from .userfas import user_add_fas_precb, user_mod_fas_precb
19 |
20 | # same procedure as standard user
21 | stageuser.takes_params += takes_params
22 |
23 | stageuser_add.register_pre_callback(user_add_fas_precb)
24 | stageuser_mod.register_pre_callback(user_mod_fas_precb)
25 |
26 |
27 | def _check_conflict(self, ldap, dn, entry, operation):
28 | """Check for conflicting login and email address
29 |
30 | The stageuser_activate plugin does not modrdn the stage user to active
31 | user. Instead it first creates a new active user DN and then deletes the
32 | stage user DN. All uniqueness plugins have to exclude stage user area and
33 | FAS has to manually check for conflicts.
34 | """
35 | unique_attrs = ["uid", "krbprincipalname", "krbcanonicalname", "mail"]
36 |
37 | if operation == "add":
38 | attr_filters = ldap.make_filter(
39 | {attr: entry[attr] for attr in unique_attrs}, rules=ldap.MATCH_ANY
40 | )
41 | elif operation == "mod":
42 | entry["uid"] = dn["uid"]
43 | attr_filters = ldap.make_filter(
44 | {attr: entry[attr] for attr in unique_attrs if attr in entry},
45 | rules=ldap.MATCH_ANY,
46 | )
47 | else:
48 | raise ValueError(operation)
49 |
50 | objcls_filters = ldap.make_filter(
51 | {"objectclass": ["posixaccount", "inetOrgPerson"]},
52 | rules=ldap.MATCH_ANY,
53 | )
54 | filters = ldap.combine_filters(
55 | [objcls_filters, attr_filters], rules=ldap.MATCH_ALL
56 | )
57 |
58 | try:
59 | res, truncated = ldap.find_entries(
60 | filters,
61 | unique_attrs,
62 | base_dn=self.api.env.basedn,
63 | scope=ldap.SCOPE_SUBTREE,
64 | )
65 | except errors.NotFound:
66 | pass
67 | else:
68 | for conflict_entry in res:
69 | if conflict_entry.dn == dn:
70 | # skip own entry
71 | continue
72 | if "mail" in entry:
73 | raise errors.DuplicateEntry(
74 | message=_(
75 | "Login '%(user)s' or email address '%(mail)s' are "
76 | "already registered."
77 | )
78 | % {"user": entry["uid"], "mail": ", ".join(entry["mail"])}
79 | )
80 | else:
81 | # mod operation
82 | raise errors.DuplicateEntry(
83 | message=_("Login '%(user)s' is already registered.")
84 | % {"user": entry["uid"]}
85 | )
86 |
87 |
88 | def stageuser_add_fas_precb(
89 | self, ldap, dn, entry, attrs_list, *keys, **options
90 | ):
91 | """Verify that uid, mail, and krb principal are not in use"""
92 | _check_conflict(self, ldap, dn, entry, operation="add")
93 | return dn
94 |
95 |
96 | stageuser_add.register_pre_callback(stageuser_add_fas_precb)
97 |
98 |
99 | def stageuser_mod_fas_precb(
100 | self, ldap, dn, entry, attrs_list, *keys, **options
101 | ):
102 | """Verify that uid, mail, and krb principal are not in use"""
103 | print(entry)
104 | _check_conflict(self, ldap, dn, entry, operation="mod")
105 | return dn
106 |
107 |
108 | stageuser_mod.register_pre_callback(stageuser_mod_fas_precb)
109 |
--------------------------------------------------------------------------------
/ipaserver/plugins/userfas.py:
--------------------------------------------------------------------------------
1 | #
2 | # FreeIPA plugin for Fedora Account System
3 | # Copyright (C) 2019 FreeIPA FAS Contributors
4 | # See COPYING for license
5 | #
6 | """FreeIPA plugin for Fedora Account System
7 | """
8 | from ipalib import _
9 | from ipalib import errors
10 | from ipalib.parameters import Flag
11 | from ipaserver.plugins.user import user
12 | from ipaserver.plugins.user import user_add
13 | from ipaserver.plugins.user import user_find
14 | from ipaserver.plugins.user import user_mod
15 | from ipaserver.plugins.user import user_show
16 |
17 | from .baseruserfas import takes_params, fas_user_attributes
18 | from .fasagreement import fasagreement_member_output_params
19 |
20 | user.takes_params += takes_params
21 |
22 | # show FAS Agreement relationship
23 | user_find.has_output_params += fasagreement_member_output_params
24 | user_mod.has_output_params += fasagreement_member_output_params
25 | user_show.has_output_params += fasagreement_member_output_params
26 |
27 | user.managed_permissions.update(
28 | {
29 | "System: Read FAS user attributes": {
30 | "replaces_global_anonymous_aci": True,
31 | "ipapermbindruletype": "all",
32 | "ipapermright": {"read", "search", "compare"},
33 | "ipapermtargetfilter": ["(objectclass=fasuser)"],
34 | "ipapermdefaultattr": {"nsAccountLock"}.union(
35 | fas_user_attributes
36 | ),
37 | },
38 | # not yet supported
39 | # "System: Self-Modify FAS user attributes": {
40 | # "replaces_global_anonymous_aci": True,
41 | # "ipapermright": {"write"},
42 | # "ipapermtargetfilter": ["(objectclass=fasuser)"],
43 | # "ipapermbindruletype": "self",
44 | # "ipapermdefaultattr": fas_user_attributes.copy(),
45 | # },
46 | },
47 | )
48 |
49 |
50 | user_find.takes_options += (
51 | Flag(
52 | "fasuser",
53 | cli_name="fasuser",
54 | doc=_("Search for FAS users"),
55 | default=False,
56 | ),
57 | )
58 |
59 |
60 | def check_fasuser_attr(entry):
61 | """Common function to verify fasuser attributes"""
62 | fasrhbzemail = entry.get("fasrhbzemail")
63 | if fasrhbzemail is not None and "@" not in fasrhbzemail:
64 | msg = _("invalid e-mail format: %(email)s")
65 | raise errors.ValidationError(
66 | name="fasrhbzemail", errors=msg % {"email": fasrhbzemail}
67 | )
68 |
69 |
70 | def user_add_fas_precb(self, ldap, dn, entry, attrs_list, *keys, **options):
71 | if any(option.startswith("fas") for option in options):
72 | # add fasuser object class
73 | if not self.obj.has_objectclass(entry["objectclass"], "fasuser"):
74 | entry["objectclass"].append("fasuser")
75 | # check fasuser attributes
76 | check_fasuser_attr(entry)
77 | return dn
78 |
79 |
80 | user_add.register_pre_callback(user_add_fas_precb)
81 |
82 |
83 | def user_mod_fas_precb(self, ldap, dn, entry, attrs_list, *keys, **options):
84 | if any(option.startswith("fas") for option in options):
85 | # add fasuser object class
86 | if "objectclass" not in entry:
87 | entry_oc = ldap.get_entry(dn, ["objectclass"])
88 | entry["objectclass"] = entry_oc["objectclass"]
89 | if not self.obj.has_objectclass(entry["objectclass"], "fasuser"):
90 | entry["objectclass"].append("fasuser")
91 | # check fasuser attributes
92 | check_fasuser_attr(entry)
93 | return dn
94 |
95 |
96 | user_mod.register_pre_callback(user_mod_fas_precb)
97 |
98 |
99 | def user_find_fas_precb(
100 | self, ldap, filter, attrs_list, base_dn, scope, criteria=None, **options
101 | ):
102 | """Search filter for FAS user"""
103 | if options.get("fasuser", False):
104 | fasfilter = ldap.make_filter(
105 | {"objectclass": ["fasuser"]}, rules=ldap.MATCH_ALL
106 | )
107 | filter = ldap.combine_filters(
108 | [filter, fasfilter], rules=ldap.MATCH_ALL
109 | )
110 | return filter, base_dn, scope
111 |
112 |
113 | user_find.register_pre_callback(user_find_fas_precb)
114 |
--------------------------------------------------------------------------------
/schema.d/89-fasschema.ldif:
--------------------------------------------------------------------------------
1 | #
2 | # FreeIPA plugin for Fedora Account System
3 | # Copyright (C) 2019 FreeIPA FAS Contributors
4 | # See COPYING for license
5 | #
6 | # Fedora Project base OID: 1.3.6.1.4.1.30401
7 | # FAS: 1.3.6.1.4.1.30401.1
8 | # Attributes: 1.3.6.1.4.1.30401.1.1
9 | # Object classes: 1.3.6.1.4.1.30401.1.2
10 | #
11 | dn: cn=schema
12 | # user extension
13 | attributeTypes: ( 1.3.6.1.4.1.30401.1.1.1 NAME 'fasTimezone' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN 'Fedora Account System' )
14 | attributeTypes: ( 1.3.6.1.4.1.30401.1.1.2 NAME 'fasLocale' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN 'Fedora Account System' )
15 | attributeTypes: ( 1.3.6.1.4.1.30401.1.1.3 NAME 'fasIRCNick' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'Fedora Account System' )
16 | attributeTypes: ( 1.3.6.1.4.1.30401.1.1.4 NAME 'fasGPGKeyId' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{40} X-ORIGIN 'Fedora Account System' )
17 | attributeTypes: ( 1.3.6.1.4.1.30401.1.1.5 NAME 'fasCreationTime' EQUALITY generalizedTimeMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 SINGLE-VALUE X-ORIGIN 'Fedora Account System' )
18 | attributeTypes: ( 1.3.6.1.4.1.30401.1.1.6 NAME 'fasStatusNote' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN 'Fedora Account System' )
19 | attributeTypes: ( 1.3.6.1.4.1.30401.1.1.7 NAME 'fasRHBZEmail' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN 'Fedora Account System' )
20 | attributeTypes: ( 1.3.6.1.4.1.30401.1.1.8 NAME 'fasGitHubUsername' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN 'Fedora Account System' )
21 | attributeTypes: ( 1.3.6.1.4.1.30401.1.1.9 NAME 'fasGitLabUsername' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN 'Fedora Account System' )
22 | attributeTypes: ( 1.3.6.1.4.1.30401.1.1.10 NAME 'fasWebsiteUrl' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'Fedora Account System' )
23 | attributeTypes: ( 1.3.6.1.4.1.30401.1.1.11 NAME 'fasIsPrivate' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE X-ORIGIN 'Fedora Account System' )
24 | attributeTypes: ( 1.3.6.1.4.1.30401.1.1.12 NAME 'fasPronoun' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'Fedora Account System' )
25 | attributeTypes: ( 1.3.6.1.4.1.30401.1.1.13 NAME 'fasRssUrl' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'Fedora Account System' )
26 | objectClasses: ( 1.3.6.1.4.1.30401.1.2.1 NAME 'fasUser' DESC 'FAS user objectclass' AUXILIARY MAY ( fasTimeZone $ fasLocale $ fasIRCNick $ fasGPGKeyId $ fasCreationTime $ fasStatusNote $ fasRHBZEmail $ fasGitHubUsername $ fasGitLabUsername $ fasWebsiteUrl $ fasIsPrivate $ fasPronoun $ fasRssUrl) X-ORIGIN 'Fedora Account System' )
27 | #
28 | # group extension
29 | attributeTypes: ( 1.3.6.1.4.1.30401.1.1.20 NAME 'fasURL' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN 'Fedora Account System' )
30 | attributeTypes: ( 1.3.6.1.4.1.30401.1.1.21 NAME 'fasMailingList' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN 'Fedora Account System' )
31 | attributeTypes: ( 1.3.6.1.4.1.30401.1.1.22 NAME 'fasIRCChannel' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'Fedora Account System' )
32 | attributeTypes: ( 1.3.6.1.4.1.30401.1.1.23 NAME 'fasDiscussionURL' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN 'Fedora Account System' )
33 | objectClasses: ( 1.3.6.1.4.1.30401.1.2.2 NAME 'fasGroup' DESC 'FAS group objectclass' SUP ipaUserGroup AUXILIARY MAY ( fasURL $ fasMailingList $ fasIRCChannel $ fasDiscussionURL) X-ORIGIN 'Fedora Account System' )
34 | #
35 | # agreements
36 | objectClasses: ( 1.3.6.1.4.1.30401.1.2.3 NAME 'fasAgreement' DESC 'FAS agreement objectclass' SUP ipaAssociation MAY ( member ) X-ORIGIN 'Fedora Account System' )
37 |
--------------------------------------------------------------------------------
/ui/js/plugins/groupfas/groupfas.js:
--------------------------------------------------------------------------------
1 | //
2 | // FreeIPA plugin for Fedora Account System
3 | // Copyright (C) 2020 FAS Contributors
4 | // See COPYING for license
5 | //
6 |
7 | define([
8 | 'freeipa/phases',
9 | 'freeipa/ipa',
10 | 'freeipa/reg'
11 | ],
12 | function(phases, IPA, reg) {
13 |
14 | // helper function
15 | function get_item(array, attr, value) {
16 |
17 | for (var i = 0, l = array.length; i < l; i++) {
18 | if (array[i][attr] === value) return array[i];
19 | }
20 | return null;
21 | }
22 |
23 | var groupfas_plugin = {};
24 |
25 | groupfas_plugin.add_group_fas_pre_op = function() {
26 | var section = {
27 | name: 'groupfas',
28 | label: '@i18n:groupfas.section',
29 | fields: [{
30 | name: 'fasgroup',
31 | $type: 'checkbox',
32 | flags: ['w_if_no_aci']
33 | },{
34 | name: 'fasurl',
35 | $type: 'multivalued',
36 | flags: ['w_if_no_aci']
37 | }, {
38 | name: 'fasmailinglist',
39 | flags: ['w_if_no_aci']
40 | }, {
41 | name: 'fasircchannel',
42 | flags: ['w_if_no_aci']
43 | }, {
44 | name: 'fasdiscussionurl',
45 | flags: ['w_if_no_aci']
46 | }]
47 | };
48 | var fasagreement = {
49 | $type: 'association',
50 | name: 'memberof_fasagreement',
51 | associator: IPA.serial_associator,
52 | add_method: 'add_group',
53 | add_title: '@i18n:fasagreement.add',
54 | remove_method: 'remove_group',
55 | remove_title: '@i18n:fasagreement.remove'
56 | };
57 |
58 | var facet = get_item(IPA.group.entity_spec.facets, '$type', 'details');
59 | // Add the details sections
60 | facet.sections.push(section);
61 | // Add the make_fasgroup action
62 | facet.actions.push('make_fasgroup');
63 | facet.header_actions.splice(-1, 0, 'make_fasgroup');
64 |
65 | // Add user agreement add/remove facet
66 | IPA.group.entity_spec.facets.push(fasagreement);
67 |
68 | /*
69 | * Now the add dialog
70 | */
71 | // Add the fasgroup checkbox on the adder dialog
72 | IPA.group.entity_spec.adder_dialog.fields.push({
73 | $type: 'checkbox',
74 | name: 'fasgroup'
75 | });
76 | // Override the dialog factory to use the checkbox's data
77 | IPA.fasgroup_adder_dialog = function(spec) {
78 | var that = IPA.group_adder_dialog(spec);
79 | var super_create_command = that.create_add_command;
80 | that.create_add_command = function(record) {
81 | var command = super_create_command(record);
82 | // If the ckeckbox is checked, add the fasgroup command option
83 | var fasgroup_field = that.fields.get_field('fasgroup');
84 | var fasgroup = fasgroup_field.save()[0];
85 | if (fasgroup) {
86 | command.set_option("fasgroup", true);
87 | }
88 | return command;
89 | };
90 | return that;
91 | }
92 | IPA.group.entity_spec.adder_dialog["$factory"] = IPA.fasgroup_adder_dialog;
93 |
94 | return true;
95 | };
96 |
97 | groupfas_plugin.add_search_group_fas = function() {
98 | var fasgroup = {
99 | name: 'fasgroup',
100 | label: '@i18n:groupfas.group',
101 | formatter: 'boolean_status'
102 | };
103 | var facet = get_item(IPA.group.entity_spec.facets, '$type', 'search');
104 | facet['columns'].splice(1, 0, fasgroup);
105 | return true;
106 | };
107 |
108 | phases.on('customization', groupfas_plugin.add_group_fas_pre_op);
109 | phases.on('customization', groupfas_plugin.add_search_group_fas);
110 |
111 | function make_fasgroup_action(spec) {
112 | spec = spec || {};
113 | spec.name = spec.name || 'make_fasgroup';
114 | spec.method = spec.method || 'mod';
115 | spec.label = spec.label || '@i18n:groupfas.make_fasgroup';
116 | spec.needs_confirm = spec.needs_confirm !== undefined ? spec.needs_confirm : true;
117 | spec.disable_cond = spec.disable_cond || ['oc_fasgroup'];
118 | spec.options = spec.options || {
119 | fasgroup: true
120 | };
121 |
122 | var that = IPA.object_action(spec);
123 |
124 | return that;
125 | }
126 |
127 | phases.on('registration', function() {
128 | reg.action.register('make_fasgroup', make_fasgroup_action);
129 | });
130 |
131 | return groupfas_plugin;
132 | });
133 |
--------------------------------------------------------------------------------
/ui/js/plugins/fasagreement/fasagreement.js:
--------------------------------------------------------------------------------
1 | //
2 | // FreeIPA plugin for Fedora Account System
3 | // Copyright (C) 2019 FreeIPA FAS Contributors
4 | // See COPYING for license
5 | //
6 |
7 | define([
8 | 'freeipa/phases',
9 | 'freeipa/ipa',
10 | 'freeipa/menu',
11 | 'freeipa/reg'
12 | ],
13 | function (phases, IPA, menu, reg) {
14 |
15 | var exp = IPA.fasagreement = {};
16 |
17 | var make_spec = function () {
18 | var spec = {
19 | name: 'fasagreement',
20 | facet_groups: ['member', 'memberuser', 'settings'],
21 | facets: [
22 | {
23 | $type: 'search',
24 | row_enabled_attribute: 'ipaenabledflag',
25 | columns: [
26 | 'cn',
27 | {
28 | name: 'ipaenabledflag',
29 | label: '@i18n:status.label',
30 | formatter: 'boolean_status'
31 | },
32 | 'description'
33 | ],
34 | actions: [
35 | 'batch_disable',
36 | 'batch_enable'
37 | ],
38 | control_buttons: [
39 | {
40 | name: 'disable',
41 | label: '@i18n:buttons.disable',
42 | icon: 'fa-minus'
43 | },
44 | {
45 | name: 'enable',
46 | label: '@i18n:buttons.enable',
47 | icon: 'fa-check'
48 | }
49 | ]
50 | },
51 | {
52 | $type: 'details',
53 | sections: [
54 | {
55 | name: 'details',
56 | fields: [
57 | 'cn',
58 | {
59 | $type: 'textarea',
60 | name: 'description'
61 | }
62 | ]
63 | }
64 | ],
65 | actions: [
66 | 'select',
67 | 'enable',
68 | 'disable',
69 | 'delete'
70 | ],
71 | header_actions: ['enable', 'disable', 'delete'],
72 | state: {
73 | evaluators: [
74 | {
75 | $factory: IPA.enable_state_evaluator,
76 | field: 'ipaenabledflag'
77 | }
78 | ],
79 | summary_conditions: [
80 | IPA.enabled_summary_cond,
81 | IPA.disabled_summary_cond
82 | ]
83 | }
84 | },
85 | {
86 | $type: 'association',
87 | name: 'member_group',
88 | add_method: 'add_group',
89 | remove_method: 'remove_group'
90 | },
91 | {
92 | $type: 'association',
93 | name: 'memberuser_user',
94 | columns: [
95 | 'uid',
96 | 'uidnumber',
97 | 'mail'
98 | ],
99 | adder_columns: [
100 | {
101 | name: 'uid',
102 | primary_key: true
103 | }
104 | ],
105 | add_method: 'add_user',
106 | remove_method: 'remove_user'
107 | }
108 | ],
109 | standard_association_facets: true,
110 | adder_dialog: {
111 | title: '@i18n:fasagreement.add',
112 | fields: [
113 | 'cn',
114 | {
115 | $type: 'textarea',
116 | name: 'description'
117 | }
118 | ]
119 | },
120 | deleter_dialog: {
121 | title: '@i18n:fasagreement.remove'
122 | }
123 | };
124 | return spec;
125 | };
126 |
127 | exp.entity_spec = make_spec();
128 |
129 | exp.fasagreement_menu_spec = {
130 | entity: 'fasagreement',
131 | label: '@i18n:fasagreement.fasagreements'
132 | };
133 |
134 | exp.register = function () {
135 | var e = reg.entity;
136 | e.register({type: 'fasagreement', spec: exp.entity_spec});
137 | };
138 |
139 | exp.add_menu_item = function () {
140 | var identity_item = menu.query({name: 'identity'});
141 | if (identity_item.length > 0) {
142 | menu.add_item(exp.fasagreement_menu_spec, 'identity');
143 | }
144 | };
145 |
146 | phases.on('registration', exp.register);
147 | phases.on('profile', exp.add_menu_item, 20);
148 |
149 | return exp;
150 | });
151 |
--------------------------------------------------------------------------------
/updates/89-fas.update:
--------------------------------------------------------------------------------
1 | #
2 | # FreeIPA plugin for Fedora Account System
3 | # Copyright (C) 2019 FreeIPA FAS Contributors
4 | # See COPYING for license
5 | #
6 |
7 | ## Make fasUser a default user object class
8 | # dn: cn=ipaConfig,cn=etc,$SUFFIX
9 | # add:ipaUserObjectClasses: fasuser
10 |
11 | # Add otp as the default auth mechanism
12 | dn: cn=ipaConfig,cn=etc,$SUFFIX
13 | only:ipaUserAuthType: otp
14 | add:ipaUserObjectClasses: fasuser
15 | only:ipaUserSearchFields: uid,givenname,sn,mail,fasIRCNick
16 |
17 |
18 | # self-service permissions to allow users to modify their own mail address
19 | # and their own FAS attributes
20 | dn: $SUFFIX
21 | add:aci: (targetattr = "mail")(version 3.0;acl "selfservice:Users can manage their own email address";allow (write) userdn = "ldap:///self";)
22 | remove:aci: (targetattr = "fasTimezone || fasLocale || fasIRCNick || fasGPGKeyId || fasRHBZEmail || fasGitHubUsername || fasGitLabUsername")(version 3.0;acl "selfservice:Users can modify their own FAS attributes";allow (write) userdn = "ldap:///self";)
23 | add:aci: (targetattr = "fasTimezone || fasLocale || fasIRCNick || fasGPGKeyId || fasRHBZEmail || fasGitHubUsername || fasGitLabUsername || fasIsPrivate")(version 3.0;acl "selfservice:Users can modify their own FAS attributes";allow (write) userdn = "ldap:///self";)
24 | remove:aci: (targetattr = "fasTimezone || fasLocale || fasIRCNick || fasGPGKeyId || fasRHBZEmail || fasGitHubUsername || fasGitLabUsername || fasIsPrivate")(version 3.0;acl "selfservice:Users can modify their own FAS attributes";allow (write) userdn = "ldap:///self";)
25 | add:aci: (targetattr = "fasTimezone || fasLocale || fasIRCNick || fasGPGKeyId || fasRHBZEmail || fasGitHubUsername || fasGitLabUsername || fasIsPrivate || fasPronoun")(version 3.0;acl "selfservice:Users can modify their own FAS attributes";allow (write) userdn = "ldap:///self";)
26 | remove:aci: (targetattr = "fasTimezone || fasLocale || fasIRCNick || fasGPGKeyId || fasRHBZEmail || fasGitHubUsername || fasGitLabUsername || fasIsPrivate || fasPronoun")(version 3.0;acl "selfservice:Users can modify their own FAS attributes";allow (write) userdn = "ldap:///self";)
27 | add:aci: (targetattr = "fasTimezone || fasLocale || fasIRCNick || fasGPGKeyId || fasRHBZEmail || fasGitHubUsername || fasGitLabUsername || fasWebsiteUrl || fasIsPrivate || fasPronoun || fasRssUrl")(version 3.0;acl "selfservice:Users can modify their own FAS attributes";allow (write) userdn = "ldap:///self";)
28 |
29 | # allow members and member managers to remove themselves from a group
30 | dn: cn=groups,cn=accounts,$SUFFIX
31 | add:aci: (targetattr = "member")(targetfilter = "(objectclass=ipaUserGroup)")(version 3.0; acl "Allow members to remove themselves"; allow (selfwrite) userattr = "member#USERDN";)
32 | add:aci: (targetattr = "memberManager")(targetfilter = "(objectclass=ipaUserGroup)")(version 3.0; acl "Allow member managers to remove themselves"; allow (selfwrite) userattr = "memberManager#USERDN";)
33 |
34 | # Index for FAS user attributes attribute
35 | dn: cn=fasIRCNick,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config
36 | default:cn: fasIRCNick
37 | default:ObjectClass: top
38 | default:ObjectClass: nsIndex
39 | default:nsSystemIndex: false
40 | add:nsIndexType: eq
41 | add:nsIndexType: pres
42 | add:nsIndexType: sub
43 |
44 | dn: cn=fasGPGKeyId,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config
45 | default:cn: fasGPGKeyId
46 | default:ObjectClass: top
47 | default:ObjectClass: nsIndex
48 | default:nsSystemIndex: false
49 | default:nsIndexType: eq
50 | default:nsIndexType: pres
51 |
52 | dn: cn=fasWebsiteUrl,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config
53 | default:cn: fasWebsiteUrl
54 | default:ObjectClass: top
55 | default:ObjectClass: nsIndex
56 | default:nsSystemIndex: false
57 | add:nsIndexType: pres
58 |
59 | dn: cn=fasRssUrl,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config
60 | default:cn: fasRssUrl
61 | default:ObjectClass: top
62 | default:ObjectClass: nsIndex
63 | default:nsSystemIndex: false
64 | add:nsIndexType: pres
65 |
66 | dn: cn=fasCreationTime,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config
67 | default:cn: fasCreationTime
68 | default:ObjectClass: top
69 | default:ObjectClass: nsIndex
70 | default:nsSystemIndex: false
71 | default:nsIndexType: eq
72 |
73 | dn: cn=fasRHBZEmail,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config
74 | default:cn: fasRHBZEmail
75 | default:ObjectClass: top
76 | default:ObjectClass: nsIndex
77 | default:nsSystemIndex: false
78 | default:nsIndexType: eq
79 | default:nsIndexType: pres
80 |
81 | dn: cn=fasGitHubUsername,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config
82 | default:cn: fasGitHubUsername
83 | default:ObjectClass: top
84 | default:ObjectClass: nsIndex
85 | default:nsSystemIndex: false
86 | default:nsIndexType: eq
87 | default:nsIndexType: pres
88 |
89 | dn: cn=fasGitLabUsername,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config
90 | default:cn: fasGitLabUsername
91 | default:ObjectClass: top
92 | default:ObjectClass: nsIndex
93 | default:nsSystemIndex: false
94 | default:nsIndexType: eq
95 | default:nsIndexType: pres
96 |
97 | dn: cn=fasIsPrivate,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config
98 | default:cn: fasIsPrivate
99 | default:ObjectClass: top
100 | default:ObjectClass: nsIndex
101 | default:nsSystemIndex: false
102 | default:nsIndexType: eq
103 |
104 | # Index account lock, used by search for disabled users
105 | dn: cn=nsAccountLock,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config
106 | default:cn: nsAccountLock
107 | default:ObjectClass: top
108 | default:ObjectClass: nsIndex
109 | default:nsSystemIndex: false
110 | default:nsIndexType: eq
111 |
112 | # ensure that email addresses are unique
113 | dn: cn=mail uniqueness,cn=plugins,cn=config
114 | default:objectClass: top
115 | default:objectClass: nsSlapdPlugin
116 | default:objectClass: extensibleObject
117 | default:cn: mail uniqueness
118 | default:nsslapd-pluginDescription: Enforce unique attribute values
119 | default:nsslapd-pluginPath: libattr-unique-plugin
120 | default:nsslapd-pluginInitfunc: NSUniqueAttr_Init
121 | default:nsslapd-pluginType: preoperation
122 | default:nsslapd-pluginEnabled: on
123 | default:uniqueness-attribute-name: mail
124 | default:uniqueness-subtrees: $SUFFIX
125 | default:uniqueness-exclude-subtrees: cn=compat,$SUFFIX
126 | default:uniqueness-exclude-subtrees: cn=staged users,cn=accounts,cn=provisioning,$SUFFIX
127 | default:nsslapd-plugin-depends-on-type: database
128 | default:nsslapd-pluginId: NSUniqueAttr
129 | default:nsslapd-pluginVersion: 1.1.0
130 | default:nsslapd-pluginVendor: Fedora Project
131 |
132 | # Default service delegation target to allow fasjson HTTP principal to
133 | # impersonate users when talking to LDAP.
134 | dn: cn=fasjson-http-delegation,cn=s4u2proxy,cn=etc,$SUFFIX
135 | default:objectClass: top
136 | default:objectClass: groupOfPrincipals
137 | default:objectClass: ipaKrb5DelegationACL
138 | default:cn: fasjson-http-delegation
139 | default:ipaAllowedTarget: cn=ipa-ldap-delegation-targets,cn=s4u2proxy,cn=etc,$SUFFIX
140 |
141 | # create the container for the user agreements
142 | dn: cn=fasagreements,$SUFFIX
143 | default: objectclass: top
144 | default: objectclass: nsContainer
145 | default: cn: fasagreements
146 |
147 | # Agreement Administrators
148 | dn: cn=FAS Agreement Administrators,cn=privileges,cn=pbac,$SUFFIX
149 | default:objectClass: nestedgroup
150 | default:objectClass: groupofnames
151 | default:objectClass: top
152 | default:cn: FAS Agreement Administrators
153 | default:description: Manage FAS user agreements
154 |
155 | # Allow users to consent to an enabled agreement but don't allow users to
156 | # retract any agreement. NOTE: fasagreement-add-user now fails with write
157 | # permission error when user is already a memberuser.
158 | dn: cn=fasagreements,$SUFFIX
159 | add:aci: (targetattr = "memberUser")(targetfilter = "(objectclass=fasagreement)")(version 3.0; acl "Forbid users to retract an agreement"; deny (selfwrite) userattr = "memberUser#USERDN";)
160 | add:aci: (targetattr = "memberUser")(targetfilter = "(&(objectclass=fasagreement)(ipaenabledflag=TRUE))")(version 3.0; acl "Allow users to consent to an agreement"; allow (selfwrite) userdn = "ldap:///all";)
161 |
162 | # Set server limits that are more fit for our project size:
163 | # - increase the pagedLookThroughLimit so we can get all users in FASJSON.
164 | dn: cn=config,cn=ldbm database,cn=plugins,cn=config
165 | replace: nsslapd-pagedlookthroughlimit:0::500000
166 |
167 | # Update Permissions (keep this at the bottom of the file)
168 | plugin: update_managed_permissions
169 |
--------------------------------------------------------------------------------
/ipaserver/plugins/groupfas.py:
--------------------------------------------------------------------------------
1 | #
2 | # FreeIPA plugin for Fedora Account System
3 | # Copyright (C) 2020 FAS Contributors
4 | # See COPYING for license
5 | #
6 | """FreeIPA plugin for Fedora Account System
7 |
8 | Modify group behavior
9 | """
10 | from ipalib import _
11 | from ipalib.parameters import Flag
12 | from ipaserver.plugins.group import group
13 | from ipaserver.plugins.group import group_add
14 | from ipaserver.plugins.group import group_add_member
15 | from ipaserver.plugins.group import group_find
16 | from ipaserver.plugins.group import group_mod
17 | from ipaserver.plugins.group import group_remove_member
18 | from ipaserver.plugins.group import group_show
19 | from ipaserver.plugins.internal import i18n_messages
20 |
21 | from .fasagreement import fasagreement_member_output_params
22 | from .fasutils import Email, IRCChannel, URL
23 |
24 | if "fasgroup" not in group.possible_objectclasses:
25 | group.possible_objectclasses.append("fasgroup")
26 |
27 | group_fas_attributes = [
28 | "fasurl",
29 | "fasmailinglist",
30 | "fasircchannel",
31 | "fasdiscussionurl",
32 | ]
33 | group.default_attributes.extend(group_fas_attributes)
34 | # always fetch objectclass so group_show can show fasgroup property
35 | if "objectclass" not in group.default_attributes:
36 | group.default_attributes.append("objectclass")
37 |
38 | # show FAS Agreement relationship
39 | group.attribute_members["memberof"].append("fasagreement")
40 | group_find.has_output_params += fasagreement_member_output_params
41 | group_mod.has_output_params += fasagreement_member_output_params
42 | group_show.has_output_params += fasagreement_member_output_params
43 |
44 | group.managed_permissions.update(
45 | {
46 | "System: Read FAS group attributes": {
47 | "replaces_global_anonymous_aci": True,
48 | "ipapermbindruletype": "all",
49 | "ipapermright": {"read", "search", "compare"},
50 | "ipapermtargetfilter": ["(objectclass=fasgroup)"],
51 | "ipapermdefaultattr": set(group_fas_attributes),
52 | },
53 | }
54 | )
55 |
56 | group.takes_params += (
57 | Flag(
58 | "fasgroup?",
59 | label=_("FAS group"),
60 | flags={"virtual_attribute", "no_create", "no_update", "no_search"},
61 | ),
62 | URL(
63 | "fasurl?",
64 | cli_name="fasurl",
65 | label=_("Group URL"),
66 | maxlength=255,
67 | ),
68 | Email(
69 | "fasmailinglist?",
70 | cli_name="fasmailinglist",
71 | label=_("Mailing list address"),
72 | maxlength=255,
73 | ),
74 | IRCChannel(
75 | "fasircchannel*",
76 | cli_name="fasircchannel",
77 | label=_("IRC network and channel"),
78 | maxlength=255,
79 | ),
80 | URL(
81 | "fasdiscussionurl?",
82 | cli_name="fasdiscussionurl",
83 | label=_("Discussion / Forum Site URL"),
84 | maxlength=255,
85 | ),
86 | )
87 |
88 | group_add.takes_options += (
89 | Flag(
90 | "fasgroup",
91 | cli_name="fasgroup",
92 | doc=_("Create a FAS group"),
93 | default=False,
94 | ),
95 | )
96 |
97 | group_find.takes_options += (
98 | Flag(
99 | "fasgroup",
100 | cli_name="fasgroup",
101 | doc=_("search for FAS groups"),
102 | default=False,
103 | ),
104 | )
105 |
106 | group_mod.takes_options += (
107 | Flag(
108 | "fasgroup",
109 | cli_name="fasgroup",
110 | doc=_("change to a FAS group"),
111 | default=False,
112 | ),
113 | )
114 |
115 |
116 | def check_fasgroup_attr(entry):
117 | """Common function to verify fasgroup attributes"""
118 | pass
119 |
120 |
121 | def get_fasgroup_attribute(self, entry_attrs, options):
122 | if options.get("raw", False):
123 | return
124 | if "fasgroup" in entry_attrs.get("objectclass", []):
125 | entry_attrs["fasgroup"] = True
126 |
127 |
128 | group.get_fasgroup_attribute = get_fasgroup_attribute
129 |
130 |
131 | def _has_fasgroup_options(options):
132 | """Check there is a FAS option in options"""
133 | # --fasgroup flag is True
134 | if options.get("fasgroup"):
135 | return True
136 | # other FAS option except 'fasgroup' (might be present and False)
137 | return any(
138 | option.startswith("fas") for option in options if option != "fasgroup"
139 | )
140 |
141 |
142 | def group_add_fas_precb(self, ldap, dn, entry, attrs_list, *keys, **options):
143 | """Add fasgroup object class and related attributes."""
144 | if _has_fasgroup_options(options):
145 | if not self.obj.has_objectclass(entry["objectclass"], "fasgroup"):
146 | entry["objectclass"].append("fasgroup")
147 | # check fasgroup attributes
148 | check_fasgroup_attr(entry)
149 | return dn
150 |
151 |
152 | group_add.register_pre_callback(group_add_fas_precb)
153 |
154 |
155 | def group_add_fas_postcb(self, ldap, dn, entry_attrs, *keys, **options):
156 | """Include fasgroup membership info"""
157 | self.obj.get_fasgroup_attribute(entry_attrs, options)
158 | return dn
159 |
160 |
161 | group_add.register_post_callback(group_add_fas_postcb)
162 |
163 |
164 | def group_find_fas_precb(
165 | self, ldap, filter, attrs_list, base_dn, scope, criteria=None, **options
166 | ):
167 | """Search filter for FAS group"""
168 | if options.get("fasgroup", False):
169 | fasfilter = ldap.make_filter(
170 | {"objectclass": ["fasgroup"]}, rules=ldap.MATCH_ALL
171 | )
172 | filter = ldap.combine_filters(
173 | [filter, fasfilter], rules=ldap.MATCH_ALL
174 | )
175 | return filter, base_dn, scope
176 |
177 |
178 | group_find.register_pre_callback(group_find_fas_precb)
179 |
180 |
181 | def group_find_fas_postcb(self, ldap, entries, truncated, *args, **options):
182 | """Search filter for FAS group"""
183 | if not options.get("raw", False):
184 | for entry in entries:
185 | self.obj.get_fasgroup_attribute(entry, options)
186 | return truncated
187 |
188 |
189 | group_find.register_post_callback(group_find_fas_postcb)
190 |
191 |
192 | def group_mod_fas_precb(self, ldap, dn, entry, *keys, **options):
193 | """Add fasgroup object class and related attributes."""
194 | if _has_fasgroup_options(options):
195 | # add fasgroup object class
196 | if "objectclass" not in entry:
197 | entry_oc = ldap.get_entry(dn, ["objectclass"])
198 | entry["objectclass"] = entry_oc["objectclass"]
199 | if not self.obj.has_objectclass(entry["objectclass"], "fasgroup"):
200 | entry["objectclass"].append("fasgroup")
201 | # check fasgroup attributes
202 | check_fasgroup_attr(entry)
203 | return dn
204 |
205 |
206 | group_mod.register_pre_callback(group_mod_fas_precb)
207 |
208 |
209 | def group_remove_member_fas_postcb(
210 | self, ldap, completed, failed, dn, entry_attrs, *keys, **options
211 | ):
212 | """Also remove user from member manager attribute"""
213 | if "user" in options:
214 | result = self.api.Command.group_remove_member_manager(
215 | keys[0], user=options["user"]
216 | )
217 | if result["completed"]:
218 | # one or more member managers were removed, update the entry
219 | entry_attrs.pop("membermanager", None)
220 | newentry = ldap.get_entry(dn, ["membermanager"])
221 | entry_attrs.update(newentry)
222 | return completed, dn
223 |
224 |
225 | group_remove_member.register_post_callback(group_remove_member_fas_postcb)
226 |
227 |
228 | def group_add_member_fas_precb(
229 | self, ldap, dn, found, not_found, *keys, **options
230 | ):
231 | """Enforce user agreements when adding new members
232 |
233 | users must consent to all agreements that are linked to this group.
234 |
235 | Limitations: The check does not work for indirect membership and does
236 | not limit existing users when a new agreement is linked to a group.
237 | """
238 | found_users = found["member"]["user"]
239 | if not found_users:
240 | # no users
241 | return dn
242 |
243 | # found some users, now check agreement
244 | group_agreements = self.api.Command.fasagreement_find(
245 | group=keys[0], pkey_only=True
246 | )["result"]
247 | if not group_agreements:
248 | # group has no agreements
249 | return dn
250 |
251 | # group has agreements
252 | group_agreements_cns = {a["cn"][0] for a in group_agreements}
253 | # rebuild found users and update not_found users
254 | # found users are a list of DNs
255 | # not found are a list of tuples (pkey, error message)
256 | found_users = found_users[:]
257 | new_found_users = found["member"]["user"] = []
258 | not_found_users = not_found["member"]["user"]
259 |
260 | for user_dn in found_users:
261 | user_uid = user_dn["uid"]
262 | # user agreements for group and user
263 | user_agreements = self.api.Command.fasagreement_find(
264 | group=keys[0], user=user_uid, pkey_only=True
265 | )["result"]
266 | user_agreements_cns = {a["cn"][0] for a in user_agreements}
267 | diff = group_agreements_cns.difference(user_agreements_cns)
268 | if diff:
269 | msg = _("missing user agreement: {}").format(
270 | ", ".join(sorted(diff))
271 | )
272 | not_found_users.append((user_uid, msg))
273 | else:
274 | new_found_users.append(user_dn)
275 |
276 | return dn
277 |
278 |
279 | group_add_member.register_pre_callback(group_add_member_fas_precb)
280 |
281 |
282 | def group_show_fas_postcb(self, ldap, dn, entry_attrs, *keys, **options):
283 | """Show fasgroup membership info"""
284 | self.obj.get_fasgroup_attribute(entry_attrs, options)
285 | return dn
286 |
287 |
288 | group_show.register_post_callback(group_show_fas_postcb)
289 |
290 |
291 | i18n_messages.messages["groupfas"] = {
292 | "section": _("Fedora Account System"),
293 | "group": _("FAS Group"),
294 | "make_fasgroup": _("Change to FAS group"),
295 | }
296 |
--------------------------------------------------------------------------------
/ipaserver/plugins/fasagreement.py:
--------------------------------------------------------------------------------
1 | #
2 | # FreeIPA plugin for Fedora Account System
3 | # See COPYING for license
4 | #
5 | """User agreement for Fedora Account System
6 |
7 | Member users are stored in "memberUser" attribute while related groups are
8 | stored in "member" attribute. FreeIPA does not have a "memberGroup" attribute.
9 | """
10 | from ipalib import Bool, Str
11 | from ipalib import errors
12 | from ipalib import output
13 | from ipalib.plugable import Registry
14 | from ipaserver.plugins.baseldap import (
15 | LDAPObject,
16 | LDAPSearch,
17 | LDAPCreate,
18 | LDAPDelete,
19 | LDAPUpdate,
20 | LDAPQuery,
21 | LDAPRetrieve,
22 | LDAPAddMember,
23 | LDAPRemoveMember,
24 | pkey_to_value,
25 | )
26 | from ipalib import _, ngettext
27 | from ipapython.dn import DN
28 | from ipaserver.plugins.internal import i18n_messages
29 |
30 | __doc__ = _(
31 | """
32 | FAS User Agreements
33 |
34 | User agreements are a concept where users may need to consent to an
35 | agreement to be able to join a group. User agreements are how we implement
36 | the FPCA for certain groups in the Fedora Accounts system.
37 |
38 | Agreements can be linked to zero to many groups. Users are able to consent
39 | to any enabled user agreement. They can neither consent to disabled user
40 | agreement nor retract consent. User agreements can be managed by admins
41 | and any user with FAS Agreement Administrators privilege.
42 |
43 | EXAMPLES:
44 |
45 | Create a new agreement:
46 | ipa fasagreement-add theagreement --desc="An agreement that needs to be agreed to"
47 |
48 | Link a group to an agreement. Linking a group to a agreement means a user cannot join
49 | that group unless they have signed the agreement:
50 | ipa fasagreement-add-group theagreement --groups=thegroup
51 |
52 | Consent to an agreement as a user:
53 | ipa fasagreement-add-user theagreement --user=myuser
54 | """
55 | )
56 |
57 |
58 | fasagreement_output_params = (
59 | Str(
60 | "memberuser_user?",
61 | label="Agreement users",
62 | ),
63 | Str(
64 | "memberusers?",
65 | label=_("Failed members"),
66 | ),
67 | )
68 |
69 | fasagreement_member_output_params = (
70 | Str(
71 | "memberof_fasagreement",
72 | label="Member of user agreement",
73 | ),
74 | )
75 |
76 | register = Registry()
77 |
78 |
79 | @register()
80 | class fasagreement(LDAPObject):
81 | """User Agreement object for FAS"""
82 |
83 | container_dn = DN(("cn", "fasagreements"))
84 | object_name = _("Agreement")
85 | object_name_plural = _("Agreements")
86 | object_class = ["ipaassociation", "fasagreement"]
87 | permission_filter_objectclasses = ["fasagreement"]
88 | default_attributes = [
89 | "cn",
90 | "description",
91 | "ipaenabledflag",
92 | "member",
93 | "memberuser",
94 | ]
95 | uuid_attribute = "ipauniqueid"
96 | attribute_members = {
97 | "memberuser": ["user"],
98 | "member": ["group"],
99 | }
100 | allow_rename = True
101 | managed_permissions = {
102 | "System: Read FAS Agreements": {
103 | "replaces_global_anonymous_aci": True,
104 | "ipapermbindruletype": "all",
105 | "ipapermright": {"read", "search", "compare"},
106 | "ipapermdefaultattr": {
107 | "objectclass",
108 | "cn",
109 | "description",
110 | "ipauniqueid",
111 | "ipaenabledflag",
112 | "member",
113 | "memberuser",
114 | },
115 | },
116 | "System: Add FAS Agreement": {
117 | "ipapermright": {"add"},
118 | "default_privileges": {"FAS Agreement Administrators"},
119 | },
120 | "System: Delete FAS Agreement": {
121 | "ipapermright": {"delete"},
122 | "default_privileges": {"FAS Agreement Administrators"},
123 | },
124 | "System: Manage FAS Agreement user membership": {
125 | "ipapermright": {"write"},
126 | "ipapermdefaultattr": {"memberUser"},
127 | "default_privileges": {"FAS Agreement Administrators"},
128 | },
129 | "System: Modify FAS Agreement": {
130 | "ipapermright": {"write"},
131 | "ipapermdefaultattr": {
132 | "cn",
133 | "description",
134 | "ipaenabledflag",
135 | "member",
136 | },
137 | "default_privileges": {"FAS Agreement Administrators"},
138 | },
139 | }
140 |
141 | label = _("User Agreements")
142 | label_singular = _("User Agreement")
143 |
144 | takes_params = (
145 | Str(
146 | "cn",
147 | cli_name="name",
148 | label=_("Agreement name"),
149 | primary_key=True,
150 | ),
151 | Str(
152 | "description?",
153 | cli_name="desc",
154 | label=_("Agreement Description"),
155 | ),
156 | Bool(
157 | "ipaenabledflag?",
158 | label=_("Enabled"),
159 | flags=["no_option"],
160 | ),
161 | )
162 |
163 |
164 | @register()
165 | class fasagreement_add(LDAPCreate):
166 | __doc__ = _("Create a new User Agreement.")
167 |
168 | has_output_params = (
169 | LDAPCreate.has_output_params + fasagreement_output_params
170 | )
171 | msg_summary = _('Added User Agreement "%(value)s"')
172 |
173 | def pre_callback(
174 | self, ldap, dn, entry_attrs, attrs_list, *keys, **options
175 | ):
176 | entry_attrs["ipaenabledflag"] = ["TRUE"]
177 | return dn
178 |
179 |
180 | @register()
181 | class fasagreement_del(LDAPDelete):
182 | __doc__ = _("Delete a User Agreement.")
183 |
184 | msg_summary = _('Deleted User Agreement "%(value)s"')
185 |
186 | def pre_callback(self, ldap, dn, *keys, **options):
187 | assert isinstance(dn, DN)
188 | try:
189 | entry = ldap.get_entry(dn, attrs_list=["member"])
190 | except errors.NotFound:
191 | raise self.obj.handle_not_found(*keys)
192 |
193 | members = entry.get("member", [])
194 | if members:
195 | raise errors.ACIError(
196 | info=_(
197 | "Not allowed to delete User Agreement with linked groups"
198 | )
199 | )
200 |
201 | return dn
202 |
203 |
204 | @register()
205 | class fasagreement_mod(LDAPUpdate):
206 | __doc__ = _("Modify a User Agreement.")
207 |
208 | has_output_params = (
209 | LDAPUpdate.has_output_params + fasagreement_output_params
210 | )
211 | msg_summary = _('Modified User Agreement "%(value)s"')
212 |
213 |
214 | @register()
215 | class fasagreement_find(LDAPSearch):
216 | __doc__ = _("Search for User Agreements.")
217 |
218 | member_attributes = ["member", "memberuser"]
219 |
220 | has_output_params = (
221 | LDAPSearch.has_output_params + fasagreement_output_params
222 | )
223 | msg_summary = ngettext(
224 | "%(count)d User Agreement matched",
225 | "%(count)d User Agreements matched",
226 | 0,
227 | )
228 |
229 |
230 | @register()
231 | class fasagreement_show(LDAPRetrieve):
232 | __doc__ = _("Display the properties of a User Agreeement.")
233 |
234 | has_output_params = (
235 | LDAPRetrieve.has_output_params + fasagreement_output_params
236 | )
237 |
238 |
239 | class _fasagreement_enabledflag(LDAPQuery):
240 | has_output = output.standard_value
241 | ipaenabledflag = None
242 |
243 | def execute(self, cn, **options):
244 | ldap = self.obj.backend
245 |
246 | dn = self.obj.get_dn(cn)
247 | try:
248 | entry_attrs = ldap.get_entry(dn, ["ipaenabledflag"])
249 | except errors.NotFound:
250 | raise self.obj.handle_not_found(cn)
251 |
252 | entry_attrs["ipaenabledflag"] = [self.ipaenabledflag]
253 |
254 | try:
255 | ldap.update_entry(entry_attrs)
256 | except errors.EmptyModlist:
257 | pass
258 |
259 | return dict(
260 | result=True,
261 | value=pkey_to_value(cn, options),
262 | )
263 |
264 |
265 | @register()
266 | class fasagreement_enable(_fasagreement_enabledflag):
267 | __doc__ = _("Enable a User Agreement")
268 |
269 | msg_summary = _('Enabled User Agreement "%(value)s"')
270 | ipaenabledflag = "TRUE"
271 |
272 |
273 | @register()
274 | class fasagreement_disable(_fasagreement_enabledflag):
275 | __doc__ = _("Disable a User Agreement")
276 |
277 | msg_summary = _('Disabled User Agreement "%(value)s"')
278 | ipaenabledflag = "FALSE"
279 |
280 |
281 | @register()
282 | class fasagreement_add_user(LDAPAddMember):
283 | __doc__ = _("Add users to a User Agreement")
284 |
285 | member_attributes = ["memberuser"]
286 | member_count_out = (_("%i user added."), _("%i users added."))
287 |
288 |
289 | @register()
290 | class fasagreement_remove_user(LDAPRemoveMember):
291 | __doc__ = _("Remove users from a User Agreement")
292 |
293 | member_attributes = ["memberuser"]
294 | member_count_out = (_("%i user removed."), _("%i users removed."))
295 |
296 | def pre_callback(self, ldap, dn, found, not_found, *keys, **options):
297 | """Remove users from linked groups"""
298 | user_uids = [
299 | user_dn["uid"] for user_dn in found["memberuser"]["user"]
300 | ]
301 | if not user_uids:
302 | # no users found
303 | return dn
304 | # check that current user has write access to modify member user
305 | # attribute of the agreement.
306 | # Note: This will fail the entire operation, not just individual
307 | # removals.
308 | if not ldap.can_write(dn, "memberuser"):
309 | raise errors.ACIError(
310 | info=(
311 | "Insufficient 'write' privilege to the 'memberuser' "
312 | "attribute of entry '{}'."
313 | ).format(dn)
314 | )
315 | # get group primary keys for agreement without loading all users
316 | group_obj = self.api.Object.group
317 | group_container_dn = DN(group_obj.container_dn, self.api.env.basedn)
318 | try:
319 | entry = ldap.get_entry(dn, ["memberuser"])
320 | except errors.NotFound:
321 | raise self.obj.handle_not_found(*keys)
322 | group_names = [
323 | group_obj.get_primary_key_from_dn(m)
324 | for m in entry["memberuser"]
325 | if m.endswith(group_container_dn)
326 | ]
327 | # remove users group groups
328 | for group_name in group_names:
329 | self.api.Command.group_remove_member(group_name, user=user_uids)
330 |
331 | return dn
332 |
333 |
334 | @register()
335 | class fasagreement_add_group(LDAPAddMember):
336 | __doc__ = _("Add group to a User Agreement")
337 |
338 | member_attributes = ["member"]
339 | member_count_out = (_("%i group added."), _("%i groups added."))
340 |
341 |
342 | @register()
343 | class fasagreement_remove_group(LDAPRemoveMember):
344 | __doc__ = _("Remove group from a User Agreement")
345 |
346 | member_attributes = ["member"]
347 | member_count_out = (_("%i group removed."), _("%i groups removed."))
348 |
349 |
350 | i18n_messages.messages["fasagreement"] = {
351 | "fasagreements": _("User Agreement"),
352 | "add": _("Add User Agrement"),
353 | "remove": _("Remove User Agrement"),
354 | }
355 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # FreeIPA plugin for Fedora Account System
2 |
3 | The *freeip-fas* plugin provides schema extension of FreeIPA users for
4 | the Fedora Account System. The plugin must be installed on all FreeIPA
5 | servers, preferable before the server/replica is installed.
6 |
7 | If the plugin is installed later, then the local schema cache may be
8 | outdated and ``ipa`` command may not be aware of the new attributes.
9 | In that case the local schema cache can be refreshed by enforcing
10 | a schema check ``ipa -eforce_schema_check=True ping`` or by purging
11 | the cache with ``rm -rf ~/.cache/ipa``.
12 |
13 | Installation requires a server upgrade ``ipa-server-upgrade`` and
14 | restart of services ``ipactl restart``. The post transaction hook
15 | of the RPM package takes care of both. A server upgrade can take a
16 | while and can disrupt normal operations on the machine. It is advised
17 | to serialize the operation and install the plugin on one server at a
18 | time.
19 |
20 | ## Additional user attributes
21 |
22 | User object is extended by a new *fasUser* object class.
23 |
24 | * *fasTimeZone*: string, writable by self
25 | * *fasLocale*: string, writable by self
26 | * *fasIRCNick*: multi-valued string, writable by self, indexed
27 | * *fasGPGKeyId*: multi-valued string, writable by self, indexed
28 | * *fasCreationTime*: timestamp, writable by admin
29 | * *fasStatusNote*: string, writable by admin
30 | * *fasRHBZEmail*: string, writable by self
31 | * *fasGitHubUsername*: string, writable by self
32 | * *fasGitLabUsername*: string, writable by self
33 | * *fasWebsiteURL*: multi-valued string, writable by self
34 | * *fasIsPrivate*: boolean, writable by self
35 | * *fasPronoun*: String, writable by self
36 | * *fasRssUrl*: multi-valued string, writable by self
37 |
38 | This also applies to stage users.
39 |
40 | ## Groups
41 |
42 | Group object is extended by a new, optional *fasGroup* object class.
43 | The object class also acts as a marker and filter to hide internal
44 | groups.
45 |
46 | * ``group_add`` and ``group_mod`` have option ``fasgroup`` to add
47 | *fasGroup* object class.
48 | * ``group_find`` has option ``fasgroup`` to filter out groups that
49 | don't have the *fasGroup* object class.
50 | * ``group_remove_member`` also removes member managers
51 |
52 | Groups with the *fasGroup* object class have the following optional attributes:
53 |
54 | * *fasURL*: multi-valued string
55 | * *fasIRCChannel*: string
56 | * *fasMailingList*: string
57 | * *fasDiscussionURL*: string
58 |
59 | ## Group / User Agreement check
60 |
61 | The ``group_add_member`` command checks user agreements. Users must
62 | consent to all linked agreements before they are permitted to join a
63 | group. The ``fasagreement-remove-user`` command removes users from
64 | groups.
65 |
66 |
67 | ## ACIs
68 |
69 | * ``Read FAS user attributes``
70 | * ``Users can modify their own FAS attributes``
71 | * ``Users can modify their own Email address``
72 | * ``Users can remove themselves as members of groups``
73 | * ``Member managers can remove themselves as member managers of groups``
74 | * ``Read FAS group attributes``
75 |
76 | ## User settings
77 |
78 | * OTP set as default authentication method.
79 | * User search uses login, given name, surname, mail, and IRC nick.
80 |
81 | ## Indexes
82 |
83 | * Index on ``fasIRCNick`` for presence and equality
84 | * Index on ``fasGPGKeyId`` for presence and equality
85 | * Index on ``fasRssUrl`` for presence
86 | * Index on ``nsAccountLock`` for equality
87 | * Uniqueness of ``mail`` attributes
88 |
89 | ## Command line extension
90 |
91 | ```
92 | $ ipa user-mod --help
93 | ...
94 | --fastimezone=STR user timezone
95 | --faslocale=STR user locale
96 | --fasircnick=STR IRC nick name
97 | --fasgpgkeyid=STR GPG Key ids
98 | --fasstatusnote=STR User status note
99 | --fascreationtime=DATETIME
100 | user creation time
101 | ...
102 | ```
103 |
104 | The `ipa stageuser-add` command is extended in the same way.
105 |
106 | Stage user plugin ensures that a stage user does not have the same
107 | login or email address as another user (active, staged, or deleted).
108 |
109 | The group add, modification, and find commands have an additional
110 | option ``--fasgroup``.
111 |
112 | ```
113 | $ ipa group-add --help
114 | ...
115 | --fasgroup create a FAS group
116 | ...
117 | $ ipa group-find --help
118 | ...
119 | --fasgroup search for FAS groups
120 | ...
121 | $ ipa group-mod --help
122 | ...
123 | --fasgroup change to a FAS group
124 | ...
125 | ```
126 |
127 | The group find and show commands also show FAS group membership.
128 |
129 | ```
130 | $ ipa group-show somegroup
131 | Group name: somegroup
132 | GID: 54400007
133 | FAS group: True
134 | $ ipa group-find somegroup
135 | ---------------
136 | 1 group matched
137 | ---------------
138 | Group name: somegroup
139 | GID: 54400007
140 | FAS group: True
141 | ----------------------------
142 | Number of entries returned 1
143 | ----------------------------
144 | ```
145 |
146 | ## User Agreements
147 |
148 | User agreements are handled by a new object type ``fasagreement``.
149 | Agreements can be linked to 0..n groups. Users are able to consent to
150 | any enabled user agreement. They can neither consent to disabled user
151 | agreement nor retract consent. User agreements can be managed by
152 | admins and any user with *FAS Agreement Administrators* privilege.
153 |
154 | ```
155 | fasagreement-add Create a new User Agreement.
156 | fasagreement-add-group Add group to a User Agreement
157 | fasagreement-add-user Add users to a User Agreement
158 | fasagreement-del Delete a User Agreement.
159 | fasagreement-disable Disable a User Agreement
160 | fasagreement-enable Enable a User Agreement
161 | fasagreement-find Search for User Agreements.
162 | fasagreement-mod Modify a User Agreement.
163 | fasagreement-remove-group Remove group from a User Agreement
164 | fasagreement-remove-user Remove users from a User Agreement
165 | fasagreement-show Display the properties of a User Agreeement.
166 | ```
167 |
168 | Permissions for privilege *FAS Agreement Administrators*:
169 |
170 | - System: Add FAS Agreement
171 | - System: Delete FAS Agreement
172 | - System: Manage FAS Agreement user membership
173 | - System: Modify FAS Agreement
174 |
175 | Permissions for all authenticated users:
176 |
177 | - System: Read FAS Agreements
178 |
179 | Additional ACIs:
180 |
181 | - Forbid users to retract an agreement
182 | - Allow users to consent to an agreement
183 |
184 | ### User Agreement example
185 |
186 | Create group as admin
187 | ```
188 | $ kinit admin
189 | Password for admin@FAS.EXAMPLE:
190 | $ ipa group-add myfasgroup
191 | ------------------------
192 | Added group "myfasgroup"
193 | ------------------------
194 | Group name: myfasgroup
195 | GID: 1632000010
196 | $ ipa fasagreement-add myagreement --desc="Agreement for myfasgroup"
197 | ----------------------------------
198 | Added User Agreement "myagreement"
199 | ----------------------------------
200 | Agreement name: myagreement
201 | Agreement Description: Agreement for myfasgroup
202 | $ ipa fasagreement-add-group myagreement --groups=myfasgroup
203 | Agreement name: myagreement
204 | Agreement Description: Agreement for myfasgroup
205 | Member groups: myfasgroup
206 | -------------------------
207 | Number of members added 1
208 | -------------------------
209 | $ ipa group-add-member myfasgroup --user=fasuser1
210 | Group name: myfasgroup
211 | GID: 1632000010
212 | Member users: fasgroupadmin, fasuser1
213 | Failed members:
214 | member user: fasuser2: missing user agreements: myagreement
215 | member group:
216 | member service:
217 | -------------------------
218 | Number of members added 0
219 | -------------------------
220 | ```
221 |
222 | Consent to agreement as normal user
223 |
224 | ```
225 | $ kinit fasuser1
226 | Password for fasuser1@FAS.EXAMPLE:
227 | $ ipa fasagreement-add-user myagreement --user=fasuser1
228 | Agreement name: myagreement
229 | Agreement Description: Agreement for myfasgroup
230 | Member groups: myfasgroup
231 | -------------------------
232 | Number of members added 1
233 | -------------------------
234 | $ ipa fasagreement-show myagreement
235 | Agreement name: myagreement
236 | Agreement Description: Agreement for myfasgroup
237 | Member groups: myfasgroup
238 | Agreement users: fasuser1
239 | ```
240 |
241 | Normal users can neither retract an agreement nor agree on behalf of
242 | another user:
243 |
244 | ```
245 | $ ipa fasagreement-add-user myagreement --user=fasuser2
246 | Agreement name: myagreement
247 | Agreement Description: Agreement for myfasgroup
248 | Failed users/groups:
249 | member user: fasuser2: Insufficient access: Insufficient 'write' privilege to the 'memberUser' attribute of entry 'cn=myagreement,cn=fasagreements,dc=fas,dc=example'.
250 | -------------------------
251 | Number of members added 0
252 | -------------------------
253 | $ ipa fasagreement-remove-user myagreement --user=fasuser1
254 | Agreement name: myagreement
255 | Agreement Description: Agreement for myfasgroup
256 | Member groups: myfasgroup
257 | Failed users/groups:
258 | member user: fasuser1: Insufficient access: Insufficient 'write' privilege to the 'memberUser' attribute of entry 'cn=myagreement,cn=fasagreements,dc=fas,dc=example'.
259 | ---------------------------
260 | Number of members removed 0
261 | ---------------------------
262 | ```
263 |
264 | Agreements cannot be removed as long as any group is linked to an
265 | agreement:
266 |
267 | ```
268 | $ kinit admin
269 | Password for admin@FAS.EXAMPLE:
270 | $ ipa fasagreement-del myagreement
271 | ipa: ERROR: Insufficient access: Not allowed to delete User Agreement with linked groups
272 | $ ipa fasagreement-remove-group myagreement --groups=myfasgroup
273 | Agreement name: myagreement
274 | Agreement Description: Agreement for myfasgroup
275 | ---------------------------
276 | Number of members removed 1
277 | ---------------------------
278 | $ ipa fasagreement-del myagreement
279 | ------------------------------------
280 | Deleted User Agreement "myagreement"
281 | ------------------------------------
282 | ```
283 |
284 | Admins and user agreement managers can remove users from an agreement.
285 | The operation also removes users all linked groups
286 |
287 | ```
288 | $ ipa group-show myfasgroup
289 | Group name: myfasgroup
290 | GID: 1632000010
291 | Member users: fasuser1
292 | Member of user agreement: myagreement
293 | $ ipa fasagreement-remove-user myagreement --user=fasuser1
294 | Agreement name: myagreement
295 | Member groups: myfasgroup
296 | ---------------------------
297 | Number of members removed 1
298 | ---------------------------
299 | $ ipa group-show myfasgroup
300 | Group name: myfasgroup
301 | GID: 1632000010
302 | ```
303 |
304 |
305 | ## Service delegation
306 |
307 | The s4u2proxy service delegation rule ``fasjson-http-delegation``
308 | allows fasjson services to impersonate users when talking to IPA's
309 | LDAP servers. All fasjson services must be added to the rule with:
310 |
311 | ```
312 | $ ipa servicedelegationrule-add-member \
313 | --principals=HTTP/$(hostname) fasjson-http-delegation
314 | ```
315 |
316 |
317 | ## Server limits
318 |
319 | With FASJSON we want to be able to list all users, using a SimplePage query. At
320 | the moment we have about 120k users and we're hitting the default
321 | ``lookThroughLimit``. We are thus increasing the ``pagedLookThroughLimit`` to
322 | 500k.
323 |
324 |
325 | ## License
326 |
327 | See file 'COPYING' for use and warranty information
328 |
329 | This program is free software; you can redistribute it and/or modify
330 | it under the terms of the GNU General Public License as published by
331 | the Free Software Foundation, either version 3 of the License, or
332 | (at your option) any later version.
333 |
334 | This program is distributed in the hope that it will be useful,
335 | but WITHOUT ANY WARRANTY; without even the implied warranty of
336 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
337 | GNU General Public License for more details.
338 |
339 | You should have received a copy of the GNU General Public License
340 | along with this program. If not, see .
341 |
--------------------------------------------------------------------------------
/COPYING:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------