548 lines
22 KiB
Python
Executable File
548 lines
22 KiB
Python
Executable File
#!/usr/bin/python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
# Copyright: (c) 2020, Sebastian Hamann
|
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
|
|
ANSIBLE_METADATA = {
|
|
'metadata_version': '1.1',
|
|
'status': ['preview'],
|
|
'supported_by': 'community'
|
|
}
|
|
|
|
DOCUMENTATION = '''
|
|
---
|
|
module: gitea_auth
|
|
|
|
short_description: Manage external authentication sources in Gitea
|
|
|
|
version_added: none
|
|
|
|
description:
|
|
- "The `gitea_auth` module allows adding, updating and removing external
|
|
authentication sources in an instance of Gitea."
|
|
|
|
requirements:
|
|
- Gitea >= 1.12.0
|
|
|
|
notes:
|
|
- Many options are required when adding new authentication sources. If the authentication source named as in I(name) already exists, the required options can be omitted.
|
|
- If I(state) is C(present), this module always reports a changed result, since Gitea does not currently provide full information about configured authentication sources.
|
|
|
|
options:
|
|
admin_filter:
|
|
description:
|
|
- An LDAP filter specifying if a user should be given administrator privileges. If a user account passes the filter, the user will be privileged as an administrator.
|
|
- Only used if I(type) is C(ldap) or C(ldap-simple) and I(state) is C(present).
|
|
type: str
|
|
required: False
|
|
auto_discover_url:
|
|
description:
|
|
- OpenID Connect auto discovery URL
|
|
- Only used if I(type) is C(oauth) and I(state) is C(present).
|
|
type: str
|
|
required: False
|
|
bind_dn:
|
|
description:
|
|
- If I(type) is C(ldap): The DN to bind to the LDAP server with when searching for the user. Omit to perform an anonymous search.
|
|
- If I(type) is C(ldap-simple): A template to use as the user's DN. The %s matching parameter will be substituted with the login name given on sign-in form.
|
|
- Only used if I(type) is C(ldap) or C(ldap-simple) and I(state) is C(present).
|
|
- Required if I(type) is C(ldap-simple) and I(state) is C(present).
|
|
type: str
|
|
required: False
|
|
bind_password:
|
|
description:
|
|
- The password for the Bind DN specified above, if any.
|
|
- Note: The password is stored in plaintext on the server. As such, ensure that the Bind DN has as few privileges as possible.
|
|
- Only used if I(type) is C(ldap) and I(state) is C(present).
|
|
type: str
|
|
required: False
|
|
client_id:
|
|
description:
|
|
- OAuth2 Client ID
|
|
- Only used if I(type) is C(oauth) and I(state) is C(present).
|
|
- Required in this case.
|
|
type: str
|
|
required: False
|
|
client_secret:
|
|
description:
|
|
- OAuth2 Client secret
|
|
- Only used if I(type) is C(oauth) and I(state) is C(present).
|
|
- Required in this case.
|
|
type: str
|
|
required: False
|
|
config:
|
|
description:
|
|
- Path to the Gitea config file (C(app.ini)).
|
|
- The config file must contain the C(RUN_USER) setting.
|
|
type: str
|
|
required: False
|
|
default: /etc/gitea/app.ini
|
|
custom_tenant_id:
|
|
description:
|
|
- Use custom Tenant ID for OAuth endpoints
|
|
- Only used if I(type) is C(oauth) and I(state) is C(present).
|
|
type: str
|
|
required: False
|
|
custom_auth_url:
|
|
description:
|
|
- Use a custom Authorization URL (option for GitLab/GitHub).
|
|
- Only used if I(type) is C(oauth) and I(state) is C(present).
|
|
type: str
|
|
required: False
|
|
custom_email_url:
|
|
description:
|
|
- Use a custom Email URL (option for GitHub).
|
|
- Only used if I(type) is C(oauth) and I(state) is C(present).
|
|
type: str
|
|
required: False
|
|
custom_profile_url:
|
|
description:
|
|
- Use a custom Profile URL (option for GitLab/GitHub).
|
|
- Only used if I(type) is C(oauth) and I(state) is C(present).
|
|
type: str
|
|
required: False
|
|
custom_token_url:
|
|
description:
|
|
- Use a custom Token URL (option for GitLab/GitHub).
|
|
- Only used if I(type) is C(oauth) and I(state) is C(present).
|
|
type: str
|
|
required: False
|
|
email_attribute:
|
|
description:
|
|
- The attribute of the user's LDAP record containing the user's email address.
|
|
- Only used if I(type) is C(ldap) or C(ldap-simple) and I(state) is C(present).
|
|
type: str
|
|
required: False
|
|
encryption:
|
|
description:
|
|
- Whether and how to use TLS when connecting to the LDAP server.
|
|
- Only used if I(type) is C(ldap) or C(ldap-simple) and I(state) is C(present).
|
|
- Required in this case.
|
|
type: str
|
|
required: False
|
|
choices: ['disable', 'starttls', 'ldaps']
|
|
firstname_attribute:
|
|
description:
|
|
- The attribute of the user's LDAP record containing the user's first name.
|
|
- Only used if I(type) is C(ldap) or C(ldap-simple) and I(state) is C(present).
|
|
type: str
|
|
required: False
|
|
host:
|
|
description:
|
|
- The host name of the LDAP server.
|
|
- Only used if I(type) is C(ldap) or C(ldap-simple) and I(state) is C(present).
|
|
- Required in this case.
|
|
type: str
|
|
required: False
|
|
name:
|
|
description:
|
|
- The name of the external authentication source.
|
|
- The name needs to be unique in the Gitea installation.
|
|
type: str
|
|
required: True
|
|
port:
|
|
description:
|
|
- The port to use when connecting to the server.
|
|
- Default is 636 if I(encryption) is C(ldaps) and otherwise 389.
|
|
- Only used if I(type) is C(ldap) or C(ldap-simple) and I(state) is C(present).
|
|
type: int
|
|
required: False
|
|
provider:
|
|
description:
|
|
- The name of an OAuth2 provider supported by Gitea. Valid names include "github", "gitlab" or "twitter", for instance.
|
|
- Only used if I(type) is C(oauth) and I(state) is C(present).
|
|
- Required in this case.
|
|
type: str
|
|
required: False
|
|
sshkey_attribute:
|
|
description:
|
|
- The attribute of the user's LDAP record containing the user's public SSH key.
|
|
- Only used if I(type) is C(ldap) or C(ldap-simple) and I(state) is C(present).
|
|
type: str
|
|
required: False
|
|
state:
|
|
description:
|
|
- Whether the authentication source should exist or not, taking action if the state is different from what is stated.
|
|
type: str
|
|
required: False
|
|
default: 'present'
|
|
choices: ['present', 'absent']
|
|
surname_attribute:
|
|
description:
|
|
- The attribute of the user's LDAP record containing the user's surname.
|
|
- Only used if I(type) is C(ldap) or C(ldap-simple) and I(state) is C(present).
|
|
type: str
|
|
required: False
|
|
sync_users:
|
|
description:
|
|
- This option enables a periodic task that synchronizes the Gitea users with the LDAP server.
|
|
- Only used if I(type) is C(ldap) and I(state) is C(present).
|
|
type: bool
|
|
required: False
|
|
default: False
|
|
type:
|
|
description:
|
|
- The type of external authentication provider to set up.
|
|
- Only used if I(state) is C(present).
|
|
type: str
|
|
required: False
|
|
choices: ['oauth', 'ldap', 'ldap-simple']
|
|
use_custom_urls:
|
|
description:
|
|
- Whether to use custom URLs for GitLab/GitHub OAuth endpoints.
|
|
- Only used if I(type) is C(oauth) and I(state) is C(present).
|
|
type: bool
|
|
required: False
|
|
user_filter:
|
|
description:
|
|
- An LDAP filter declaring when a user should be allowed to log in. The %s matching parameter will be substituted with login name given on sign-in form.
|
|
- Only used if I(type) is C(ldap) or C(ldap-simple) and I(state) is C(present).
|
|
- Required in this case.
|
|
type: str
|
|
required: False
|
|
user_search_base:
|
|
description:
|
|
- The LDAP base at which user accounts will be searched for.
|
|
- Only used if I(type) is C(ldap) or C(ldap-simple) and I(state) is C(present).
|
|
- Required if I(type) is C(ldap) and I(state) is C(present).
|
|
type: str
|
|
required: False
|
|
username_attribute:
|
|
description:
|
|
- The attribute of the user's LDAP record containing the user name. The attribute value will be used for new Gitea accounts' user name after the first successful sign-in. Leave empty to use the login name given on sign-in form.
|
|
- This is useful when the supplied login name is matched against multiple attributes, but only a single specific attribute should be used for the Gitea account name.
|
|
- Only used if I(type) is C(ldap) or C(ldap-simple) and I(state) is C(present).
|
|
type: str
|
|
required: False
|
|
|
|
author:
|
|
- Sebastian Hamann (@s-hamann)
|
|
'''
|
|
|
|
EXAMPLES = '''
|
|
# Create an OAuth2 authentication source
|
|
- name: Enable login with GitHub
|
|
gitea_auth:
|
|
name: GitHub
|
|
type: oauth
|
|
provider: github
|
|
client_id: gitea
|
|
client_secret: some_token
|
|
|
|
# Create an LDAP authentication source
|
|
- name: Enable LDAP login
|
|
gitea_auth:
|
|
name: OpenLDAP
|
|
type: ldap
|
|
host: ldap.my.domain
|
|
encryption: starttls
|
|
bind_dn: uid=gitea,ou=machines,dc=my,dc=domain
|
|
bind_password: some_password
|
|
user_search_base: ou=people,dc=my,dc=domain
|
|
user_filter: '(&(objectClass=posixAccount)(uid=%s)(memberOf=cn=Gitea Users,ou=groups,dc=my,dc=domain))'
|
|
admin_filter: '(memberOf=cn=Gitea Admins,ou=groups,dc=my,dc=domain)'
|
|
username_attribute: uid
|
|
firstname_attribute: givenName
|
|
surname_attribute: sn
|
|
email_attribute: mail
|
|
sshkey_attribute: sshPublicKey
|
|
sync_users: true
|
|
|
|
# Create an LDAP authentication source
|
|
- name: Enable Active Directory login
|
|
gitea_auth:
|
|
name: Active Directory
|
|
type: ldap
|
|
host: dc.my.domain
|
|
encryption: ldaps
|
|
bind_dn: uid=gitea,ou=machines,dc=my,dc=domain
|
|
bind_password: some_password
|
|
user_search_base: ou=people,dc=my,dc=domain
|
|
user_filter: '(&(objectCategory=Person)(memberOf=cn=Gitea Users,ou=groups,dc=my,dc=domain)(sAMAccountName=%s)(!(UserAccountControl:1.2.840.113556.1.4.803:=2)))'
|
|
admin_filter: '(memberOf=cn=Gitea Admins,ou=groups,dc=my,dc=domain)'
|
|
username_attribute: sAMAccountName
|
|
firstname_attribute: givenName
|
|
surname_attribute: sn
|
|
email_attribute: mail
|
|
sync_users: true
|
|
|
|
# Delete an authentication source
|
|
- name: Remove login with GitHub
|
|
gitea_auth:
|
|
name: GitHub
|
|
state: absent
|
|
'''
|
|
|
|
RETURN = '''
|
|
'''
|
|
|
|
import os
|
|
import pwd
|
|
import subprocess
|
|
|
|
from ansible.module_utils.basic import AnsibleModule
|
|
from collections import namedtuple
|
|
|
|
|
|
AuthSrc = namedtuple('AuthSrc', ['id', 'name', 'type', 'enabled'])
|
|
CommandResult = namedtuple('CommandResult', ['stdout', 'stderr', 'returncode'])
|
|
|
|
|
|
def gitea_cmd(command, app_ini_path):
|
|
"""Run the given Gitea auth command and return the output.
|
|
|
|
:command: The auth command to run, as a list (e.g. ['delete', '--id', '1'])
|
|
:app_ini_path: The absolute path to the configuration file (app.ini)
|
|
:returns: The output and return code of the given command as a named tuple
|
|
(stdout, stderr, returncode)
|
|
|
|
"""
|
|
|
|
def become_gitea(uid, gid):
|
|
"""Return a function that changes the uid and gid to the given
|
|
user and group."""
|
|
def result():
|
|
os.setgroups([gid])
|
|
os.setgid(gid)
|
|
os.setuid(uid)
|
|
return result
|
|
|
|
import configparser
|
|
app_ini = configparser.ConfigParser()
|
|
app_ini.read(app_ini_path)
|
|
user = app_ini['DEFAULT']['RUN_USER']
|
|
|
|
userinfo = pwd.getpwnam(user)
|
|
uid = userinfo.pw_uid
|
|
gid = userinfo.pw_gid
|
|
home = userinfo.pw_dir
|
|
|
|
env = os.environ.copy()
|
|
env['HOME'] = home
|
|
|
|
cmd = subprocess.Popen(['gitea', '--config', app_ini_path, 'admin', 'auth'] + command,
|
|
preexec_fn=become_gitea(uid, gid), cwd=home, env=env,
|
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
(stdout, stderr) = cmd.communicate()
|
|
return CommandResult(stdout, stderr, cmd.returncode)
|
|
|
|
|
|
def run_module():
|
|
# define available arguments/parameters a user can pass to the module
|
|
module_args = dict(
|
|
admin_filter=dict(type='str'),
|
|
auto_discover_url=dict(type='str'),
|
|
bind_dn=dict(type='str'),
|
|
bind_password=dict(type='str', no_log=True),
|
|
client_id=dict(type='str'),
|
|
client_secret=dict(type='str', no_log=True),
|
|
config=dict(type='str', default='/etc/gitea/app.ini'),
|
|
custom_tenant_id=dict(type='str'),
|
|
custom_auth_url=dict(type='str'),
|
|
custom_email_url=dict(type='str'),
|
|
custom_profile_url=dict(type='str'),
|
|
custom_token_url=dict(type='str'),
|
|
email_attribute=dict(type='str'),
|
|
encryption=dict(type='str', choices=['disable', 'starttls', 'ldaps']),
|
|
firstname_attribute=dict(type='str'),
|
|
host=dict(type='str'),
|
|
name=dict(type='str', required=True),
|
|
port=dict(type='int'),
|
|
provider=dict(type='str'),
|
|
sshkey_attribute=dict(type='str'),
|
|
state=dict(type='str', default='present', choices=['present', 'absent']),
|
|
surname_attribute=dict(type='str'),
|
|
sync_users=dict(type='bool', default=False),
|
|
type=dict(type='str', choices=['oauth', 'ldap', 'ldap-simple']),
|
|
use_custom_urls=dict(type='bool'),
|
|
user_filter=dict(type='str'),
|
|
user_search_base=dict(type='str'),
|
|
username_attribute=dict(type='str')
|
|
)
|
|
|
|
# seed the result dict in the object
|
|
result = dict(
|
|
changed=False,
|
|
)
|
|
|
|
module = AnsibleModule(
|
|
argument_spec=module_args,
|
|
supports_check_mode=True
|
|
)
|
|
|
|
auth_providers = []
|
|
|
|
# Get the currently configured authentication sources.
|
|
header_pos = {}
|
|
for line in gitea_cmd(['list'], module.params['config'])[0].splitlines():
|
|
# Gitea may print random cruft before the actual information, i.e. the
|
|
# header may not be in the first line. Search it.
|
|
line = line.decode().split()
|
|
if not header_pos:
|
|
if line[0] == 'ID':
|
|
header_pos['id'] = line.index('ID')
|
|
header_pos['name'] = line.index('Name')
|
|
header_pos['type'] = line.index('Type')
|
|
header_pos['enabled'] = line.index('Enabled')
|
|
continue
|
|
else:
|
|
a = AuthSrc(id=int(line[header_pos['id']]),
|
|
name=line[header_pos['name']],
|
|
type=line[header_pos['type']],
|
|
enabled=line[header_pos['enabled']].lower() == 'true'
|
|
)
|
|
auth_providers.append(a)
|
|
|
|
# Set `id` to the ID of the authentication source with the given name, if any.
|
|
for p in auth_providers:
|
|
if p.name == module.params['name']:
|
|
id = p.id
|
|
break
|
|
else:
|
|
id = None
|
|
|
|
# Sanity checks on the parameters.
|
|
if module.params['state'] == 'present' and not module.params['type']:
|
|
module.fail_json(rc=256, msg='type is required with state=present')
|
|
if module.params['state'] == 'present' and id is None:
|
|
if module.params['type'] == 'oauth':
|
|
required_params = ['provider', 'client_id', 'client_secret']
|
|
elif module.params['type'] == 'ldap':
|
|
required_params = ['host', 'encryption', 'user_filter', 'user_search_base']
|
|
elif module.params['type'] == 'ldap-simple':
|
|
required_params = ['host', 'encryption', 'user_filter', 'bind_dn']
|
|
missing_params = []
|
|
for p in required_params:
|
|
if module.params[p] is None:
|
|
missing_params.append(p)
|
|
if missing_params:
|
|
msg = ('The following parameters are required: {lst}'.
|
|
format(lst=', '.join(missing_params)))
|
|
module.fail_json(rc=256, msg=msg)
|
|
|
|
if module.params['state'] == 'absent' and id is not None:
|
|
# Delete an authentication source.
|
|
if not module.check_mode:
|
|
retval = gitea_cmd(['delete', '--id', str(id)], module.params['config'])
|
|
if retval.returncode > 0:
|
|
msg = ('Could not delete authentication source {name}'.
|
|
format(name=module.params['name']))
|
|
module.fail_json(msg=msg, stdout=retval.stdout, rc=retval.returncode, **result)
|
|
result['changed'] = True
|
|
|
|
elif module.params['state'] == 'present':
|
|
# Add/update an authentication source.
|
|
if module.params['type'] == 'oauth':
|
|
if id is None:
|
|
cmd = ['add-oauth']
|
|
else:
|
|
cmd = ['update-oauth', '--id', str(id)]
|
|
cmd += ['--name', module.params['name']]
|
|
if module.params['provider']:
|
|
cmd += ['--provider', module.params['provider']]
|
|
if module.params['client_id']:
|
|
cmd += ['--key', module.params['client_id']]
|
|
if module.params['client_secret']:
|
|
cmd += ['--secret', module.params['client_secret']]
|
|
if module.params['auto_discover_url']:
|
|
cmd += ['--auto-discover-url', module.params['auto_discover_url']]
|
|
if module.params['use_custom_urls']:
|
|
cmd += ['--use-custom-urls', str(module.params['use_custom_urls'])]
|
|
if module.params['custom_tenant_id']:
|
|
cmd += ['--custom-tenant-id', module.params['custom_tenant_id']]
|
|
if module.params['custom_auth_url']:
|
|
cmd += ['--custom-auth-url', module.params['custom_auth_url']]
|
|
if module.params['custom_token_url']:
|
|
cmd += ['--custom-token-url', module.params['custom_token_url']]
|
|
if module.params['custom_profile_url']:
|
|
cmd += ['--custom-profile-url', module.params['custom_profile_url']]
|
|
if module.params['custom_email_url']:
|
|
cmd += ['--custom-email-url', module.params['custom_email_url']]
|
|
|
|
elif module.params['type'] == 'ldap' or module.params['type'] == 'ldap-simple':
|
|
if module.params['type'] == 'ldap':
|
|
if id is None:
|
|
cmd = ['add-ldap']
|
|
else:
|
|
cmd = ['update-ldap', '--id', str(id)]
|
|
if module.params['bind_dn']:
|
|
cmd += ['--bind-dn', module.params['bind_dn']]
|
|
cmd += ['--attributes-in-bind']
|
|
if module.params['bind_password']:
|
|
cmd += ['--bind-password', module.params['bind_password']]
|
|
if module.params['sync_users']:
|
|
cmd += ['--synchronize-users']
|
|
|
|
elif module.params['type'] == 'ldap-simple':
|
|
if id is None:
|
|
cmd = ['add-ldap-simple']
|
|
else:
|
|
cmd = ['update-ldap-simple', '--id', str(id)]
|
|
if module.params['bind_dn']:
|
|
cmd += ['--user-dn', module.params['bind_dn']]
|
|
|
|
cmd += ['--name', module.params['name']]
|
|
if module.params['host']:
|
|
cmd += ['--host', module.params['host']]
|
|
if module.params['port']:
|
|
cmd += ['--port', module.params['port']]
|
|
elif id is None:
|
|
if module.params['encryption'] == 'ldaps':
|
|
port = '636'
|
|
else:
|
|
port = '389'
|
|
cmd += ['--port', port]
|
|
if module.params['encryption']:
|
|
if module.params['encryption'] == 'disable':
|
|
encryption = 'unencrypted'
|
|
elif module.params['encryption'] == 'starttls':
|
|
encryption = 'StartTLS'
|
|
elif module.params['encryption'] == 'ldaps':
|
|
encryption = 'LDAPS'
|
|
cmd += ['--security-protocol', encryption]
|
|
if module.params['user_search_base']:
|
|
cmd += ['--user-search-base', module.params['user_search_base']]
|
|
if module.params['user_filter']:
|
|
cmd += ['--user-filter', module.params['user_filter']]
|
|
if module.params['admin_filter']:
|
|
cmd += ['--admin-filter', module.params['admin_filter']]
|
|
if module.params['username_attribute']:
|
|
cmd += ['--username-attribute', module.params['username_attribute']]
|
|
if module.params['firstname_attribute']:
|
|
cmd += ['--firstname-attribute', module.params['firstname_attribute']]
|
|
if module.params['surname_attribute']:
|
|
cmd += ['--surname-attribute', module.params['surname_attribute']]
|
|
if module.params['email_attribute'] or id is None:
|
|
email_attribute = module.params['email_attribute']
|
|
if not email_attribute:
|
|
email_attribute = 'mail'
|
|
cmd += ['--email-attribute', email_attribute]
|
|
if module.params['sshkey_attribute']:
|
|
cmd += ['--public-ssh-key-attribute', module.params['sshkey_attribute']]
|
|
|
|
if not module.check_mode:
|
|
retval = gitea_cmd(cmd, module.params['config'])
|
|
if retval.returncode > 0:
|
|
if id is None:
|
|
verb = 'add'
|
|
else:
|
|
verb = 'update'
|
|
msg = ('Could not {verb} authentication source {name}'.
|
|
format(verb=verb, name=module.params['name']))
|
|
module.fail_json(msg=msg, stdout=retval.stdout, rc=retval.returncode, **result)
|
|
|
|
# We can not know if anything was changed, since we can not get the
|
|
# full configuration of an authentication source out of Gitea.
|
|
result['changed'] = True
|
|
|
|
module.exit_json(**result)
|
|
|
|
|
|
def main():
|
|
run_module()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|