ansible-gitea/library/gitea_auth.py
2023-12-08 12:36:24 +01:00

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()