From e09af1508594ff259ff5a87795a5475759c2685c Mon Sep 17 00:00:00 2001 From: bepting Date: Fri, 8 Dec 2023 12:36:24 +0100 Subject: [PATCH] . --- README.md | 328 ++++++++++++++++++ defaults/main.yml | 23 ++ filter_plugins/filters.py | 15 + handlers/main.yml | 19 ++ library/gitea_auth.py | 547 +++++++++++++++++++++++++++++++ meta/main.yml | 28 ++ tasks/get_secrets.yml | 25 ++ tasks/install_gitea_binary.yml | 144 ++++++++ tasks/main.yml | 266 +++++++++++++++ templates/app.ini.j2 | 10 + templates/gitea.openrc.j2 | 45 +++ templates/gitea.service.j2 | 32 ++ templates/gitea_override.conf.j2 | 38 +++ vars/alpine.yml | 8 + vars/archlinux.yml | 3 + vars/default.yml | 9 + vars/gentoo.yml | 3 + vars/main.yml | 74 +++++ 18 files changed, 1617 insertions(+) create mode 100644 README.md create mode 100644 defaults/main.yml create mode 100755 filter_plugins/filters.py create mode 100644 handlers/main.yml create mode 100755 library/gitea_auth.py create mode 100644 meta/main.yml create mode 100644 tasks/get_secrets.yml create mode 100644 tasks/install_gitea_binary.yml create mode 100644 tasks/main.yml create mode 100644 templates/app.ini.j2 create mode 100644 templates/gitea.openrc.j2 create mode 100644 templates/gitea.service.j2 create mode 100644 templates/gitea_override.conf.j2 create mode 100644 vars/alpine.yml create mode 100644 vars/archlinux.yml create mode 100644 vars/default.yml create mode 100644 vars/gentoo.yml create mode 100644 vars/main.yml diff --git a/README.md b/README.md new file mode 100644 index 0000000..f200f57 --- /dev/null +++ b/README.md @@ -0,0 +1,328 @@ +Gitea +===== + +This role sets up and configures a [Gitea](https://gitea.io/) instance. +It supports official binaries from https://gitea.io/ or distribution-provided packages. +Local user accounts can be created on deployment. +It is also possible to configure external authentication sources. + +Requirements +------------ + +Gitea versions older than 1.18.0 are not (fully) supported. +Depending on the exact configuration, they may or may not work. + +If TLS encryption (i.e. HTTPS) is desired, the target system needs to have a suitable X.509 certificate. +This roles does not handle deploying certificates. + +Gitea needs a database server, unless it is configured to use SQLite. +This role does not handle database configuration. + +Gitea's (optional) email system requires a SMTP server or a working `sendmail` program. +This is not set up by this role either. + +Role Variables +-------------- + +* `gitea_use_pkg` + Whether to prefer the distribution's package of Gitea. + Defaults to `true` but is set to `false` if the distribution is not known provide a package. +* `gitea_version` + What version of Gitea to install from . + If left unset, the latest version (not including release candidates) is chosen. + This setting is ignored when using a distribution package (cf. `gitea_use_pkg`). +* `gitea_bind_address` + The IP address to bind to. + Set to `0.0.0.0` to listen on all IP addresses. + Defaults to `127.0.0.1`. +* `gitea_port` + The TCP port to listen on. + Defaults to `443` if `gitea_tls_cert` is set and `80` if it is not. +* `gitea_tls_cert` + Path to a PEM-encoded X.509 certificate for Gitea to use. + The file needs to exist and be readable by the Gitea user. + Default is unset, which disables TLS support. +* `gitea_tls_cert_key` + Path to the PEM-encoded private key file for the certificate. + The file needs to exist and be readable by the Gitea user. + Default is unset. +* `gitea_user`, `gitea_group` + The system user account and system group to run Gitea as. + `gitea_user` defaults to `git`; `gitea_group` defaults to the user name. + When using a distribution package (cf. `gitea_use_pkg`), these settings are ignored. +* `gitea_data_path` + The path where repositories, user avatars and similar data is stored. + Defaults to `/var/lib/gitea`. +* `gitea_log_path` + The directory where Gitea's log files are stored. + Gitea creates a number of log files for different purposes. + Defaults to `/var/log/gitea`. +* `gitea_loglevel` + The log level Gitea's various loggers. + Valid values are `Trace`, `Debug`, `Info`, `Warn`, `Error`, `Critical`, `Fatal` and `None` in decreasing order of verbosity. + Fine tuning individual loggers is possible using `gitea_extra_options`. + Defaults to `Info`. +* `gitea_custom_path` + The path where custom files can be placed. + These files allow [customizing Gitea](https://docs.gitea.io/en-us/customizing-gitea/). + `gitea_custom_files` can be used to deploy files to this path. + Note: For security reasons, setting this to a directory within `gitea_data_path` is not recommended. + Defaults to `/etc/gitea/custom`. +* `gitea_custom_files` + Path to a directory on the Ansible controller that contains files that should be deployed to `gitea_custom_path`. + Refer to the [Gitea documentation](https://docs.gitea.io/en-us/customizing-gitea/) for details. + Optional. +* `gitea_database_type` + The type of database that Gitea should use to store user information, repository metadata, issues, etc. + Valid values are `mysql`, `postgres`, `mssql` and `sqlite3`. + Note that this role does not set up a database for Gitea. + This should be done by another role, unless using SQLite, which does not need any setup. + Mandatory. +* `gitea_database_host` + The host name (and optionally port) of the database system. + This can also be an absolute path to a UNIX socket, if the database runs on the same system as Gitea. + Mandatory, unless `gitea_database_type` is `sqlite3`. +* `gitea_database_name` + The name of the database to use. + When using SQLite, this is the path to the database file. + If not set, Gitea's internal default value is used. +* `gitea_database_user` + User account to use when connecting to the database. + Mandatory, unless `gitea_database_type` is `sqlite3`. +* `gitea_database_password` + The password for `gitea_database_user`. + Omit, if the database does not require a password. +* `gitea_enable_mailer` + Whether to enable the mailer (for password resets, etc.) and email notifications. + Requires are working SMTP server, somewhere. + Setting up an SMTP server is outside the scope of this role. + Defaults to `false` unless `gitea_mailer_host` is set. +* `gitea_mailer_host` + The host name (and optionally port) of a SMTP server to use for sending email. + If the mailer is enabled without setting this option, the system's `sendmail` command is used. + Optional. +* `gitea_mailer_from` + The sender address for mail generated by Gitea in RFC 5322 format. + Mandatory if the mailer is enabled. +* `gitea_mailer_user` + User account to use when connecting to the SMTP server. + Optional. +* `gitea_mailer_password` + The password of `gitea_mailer_user`. + Optional. +* `gitea_enable_indexer` + Whether to enable the repository indexer. + The indexer provides code search, but is known to use a fairly large amount of disk space. + Defaults to `true`. +* `gitea_enable_lfs` + Whether to enable git-lfs support for storing large file more efficiently. + Defaults to `false`. +* `gitea_enable_signing` + Whether to enable automatic singing of commits that are created via the web interface (e.g. on merges or repository initialisation). + For this purpose a PGP key is generated and stored on the remote system. + Note that a key is generated only once, and not regenerated, e.g. when the configured identity or other key parameters are changed. + The exact conditions on when a signature is made can be fine tuned using `gitea_extra_options`. + Defaults to `true`. +* `gitea_signing_key_type` + The type of PGP key to generate. + Valid values depend on the capabilities of the `gpg` program on the remote system. + Defaults to `RSA`. +* `gitea_signing_key_length` + Length of the PGP key in bits. + Defaults to `4096`. +* `gitea_committer_name` + If `gitea_enable_signing` is `true`, this is the name in the signing PGP key. + The value may be used elsewhere as well. + Defaults to `Gitea Bot`. +* `gitea_committer_email` + If `gitea_enable_signing` is `true`, this is the email address in the signing PGP key. + The value may be used elsewhere as well. + The email address does not need to exist. + Defaults to `invalid`. + Another generically useful value might be `{{ gitea_user }}@{{ ansible_facts['hostname'] }}`. +* `gitea_users` + A list of local user account to set up within Gitea. + Note that this only allows creating users, but not modifying existing users. + Each list item is in turn a dictionary with the following keys: + * `name` + The user's name. + Needs to be unique within the Gitea installation. + Mandatory. + * `email` + The user's email address. + Needs to be unique within the Gitea installation. + Mandatory. + * `password` + The user's password. + Mandatory. + * `admin` + Whether to assign administrative privileges to the user. + Defaults to `false`. + Note: `gitea_users` only works on Gitea 1.14.0 or newer. +* `gitea_auth_providers` + A list of [external authentication](https://docs.gitea.io/en-us/authentication/) sources to set up within Gitea. + Each list item is in turn a dictionary with the following keys: + * `name` + The name of the authentication source. + Needs to be unique within the Gitea installation. + Mandatory. + * `type` + The type of external authentication source to configure. + Valid values are `oauth`, `ldap` and `ldap-simple`. + Mandatory. + If `type` is `oauth`, the following keys are used: + * `provider` + The name of the OAuth2 provider. + Valid values are the names of providers supported by Gitea, such as `github`, `gitlab` or `twitter`. + Mandatory. + * `client_id` + The client ID for use with the OAuth2 provider. + Mandatory. + * `client_secret` + The client secret for use with the OAuth2 provider. + Mandatory. + * `auto_discover_url` + The OpenID auto discovery URL. + Optional. + * `use_custom_urls` + Whether to use custom URLs if `provider` is `github`, `gitlab` or `gitea`. + Defaults to `false`. + * `custom_tenant_id` + A custom Tenant ID for OAuth2 endpoint (cf. `use_custom_urls`). + Optional. + Only works on Gitea 1.19.0 an newer. + * `custom_auth_url` + A custom Authorization URL (cf. `use_custom_urls`). + Optional. + * `custom_email_url` + A custom Email URL (cf. `use_custom_urls`). + Optional. + * `custom_profile_url` + A custom Profile URL (cf. `use_custom_urls`). + Optional. + * `custom_token_url` + A custom Token URL (cf. `use_custom_urls`). + Optional. + If `type` is `ldap` or `ldap-simple`, the following keys are used: + * `host` + The host name of the LDAP server to connect to. + Mandatory. + * `port` + The TCP port the LDAP service runs on. + Defaults to `389` or, if `encryption` is `ldaps`, to `636`. + * `encryption` + How connections to the LDAP server should be encrypted. + Valid values are `disable`, `starttls` and `ldaps`. + Mandatory. + * `bind_dn` + If `type` is `ldap`: The DN to bind to the LDAP server with when searching for the user. Omit to perform an anonymous search. + If `type` is `ldap-simple`: A template to use as the user's DN. `%s` is substituted with the login name given on sign-in form. + Mandatory if `type` is `ldap-simple`. + * `bind_password` + The password for the user in `bind_dn`. + Only used when `type` is `ldap`. + * `user_search_base` + The LDAP base at which user accounts will be searched for. + Mandatory if `type` is `ldap`. + * `user_filter` + An LDAP filter declaring when a user should be allowed to log in. + `%s` is substituted with login name given on sign-in form. + Mandatory. + * `admin_filter` + 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. + Optional. + * `username_attribute` + 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. + Optional. + * `email_attribute` + The attribute of the user's LDAP record containing the user's email address. + Defaults to `mail`. + * `firstname_attribute` + The attribute of the user's LDAP record containing the user's first name. + Optional. + * `surname_attribute` + The attribute of the user's LDAP record containing the user's surname. + Optional. + * `sshkey_attribute` + The attribute of the user's LDAP record containing the user's public SSH key. + Optional. + * `sync_users` + This option enables a periodic task that synchronizes the Gitea users with the LDAP server. + Defaults to `false`. + Note: `gitea_auth_providers` only works on Gitea 1.12.0 or newer. +* `gitea_extra_options` + Additional configuration options for Gitea. + This variable is a dictionary where the keys are section names in `app.ini`. + The values are in turn dictionaries where keys are Gitea configuration options for the appropriate section and values are the corresponding configuration values. + Refer to the [Gitea documentation](https://docs.gitea.io/en-us/config-cheat-sheet/) for options and their meaning. + Optional. +* `gitea_extra_groups` + A list of groups that the Gitea system user is added to. + This allows granting access to additional resources, such as the private key file. + All groups need to exist on the target system; this role does not create them. + Empty by default. +* `gitea_inaccessible_paths` + If the target system uses systemd, this option takes a list of paths, that should not be accessible at all for Gitea. + Regardless of this option, home directories are made inaccessible. + Optional. + +Dependencies +------------ + +This role does not set up TLS certificates and therefore depends on a role that generates and deploys them, if TLS support is desired. + +It also depends on a role to set up a MySQL/MariaDB, PostgreSQL or Microsoft SQL Server, respectively (possibly on a different system), if using a "full" DBMS is desired. +Alternatively, SQLite can be used, which does not require any further setup. + +Example Configuration +--------------------- + +The following is a short example for some of the configuration options this role provides: + +```yaml +gitea_bind_address: '0.0.0.0' +gitea_database_type: 'mysql' +gitea_database_host: '/run/mysqld/mysqld.sock' +gitea_database_name: 'gitea' +gitea_database_user: 'git' +gitea_inaccessible_paths: + - '/var/lib/mysql' +gitea_enable_mailer: true +gitea_mailer_from: "{{ gitea_user }}@{{ ansible_facts['fqdn'] }}" +gitea_extra_options: + server: + LANDING_PAGE: 'explore' + service: + DISABLE_REGISTRATION: true +gitea_users: + - name: 'admin user' + email: 'admin@my.domain' + password: 'admin_password' + admin: true + - name: 'ordinary user' + email: 'user@my.domain' + password: 'user_password' +gitea_auth_providers: + - name: 'OpenLDAP' + type: 'ldap' + host: 'localhost' + encryption: 'disable' + bind_dn: 'cn=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))' + admin_filter: '(memberOf=cn=Gitea Admins,ou=groups,dc=my,dc=domain)' + username_attribute: 'uid' + email_attribute: 'mail' + sshkey_attribute: 'sshPublicKey' + sync_users: true +``` + +License +------- + +MIT diff --git a/defaults/main.yml b/defaults/main.yml new file mode 100644 index 0000000..3f96202 --- /dev/null +++ b/defaults/main.yml @@ -0,0 +1,23 @@ +--- +gitea_use_pkg: true +gitea_user: 'git' +gitea_group: "{{ gitea_user }}" +gitea_extra_groups: [] +gitea_data_path: '/var/lib/gitea' +gitea_log_path: '/var/log/gitea' +gitea_custom_path: "/etc/gitea/custom" +gitea_inaccessible_paths: [] +gitea_bind_address: '127.0.0.1' +gitea_port: "{{ (gitea_tls_cert is defined) | ternary(443, 80) }}" +gitea_loglevel: 'Info' +gitea_enable_mailer: "{{ gitea_mailer_host is defined }}" +gitea_enable_lfs: false +gitea_enable_indexer: true +gitea_enable_signing: true +gitea_committer_name: 'Gitea Bot' +gitea_committer_email: 'invalid' +gitea_signing_key_type: 'RSA' +gitea_signing_key_length: 4096 +gitea_extra_options: {} +gitea_users: [] +gitea_auth_providers: [] diff --git a/filter_plugins/filters.py b/filter_plugins/filters.py new file mode 100755 index 0000000..31d5669 --- /dev/null +++ b/filter_plugins/filters.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +class FilterModule(object): + def filters(self): + return { + 'sort_versions': self.sort_versions, + } + + def sort_versions(self, value): + """This is a somewhat naive approach to version sorting. It only + supports versions that consist of numbers separated by dots.""" + return sorted(value, key=lambda s: list(map(int, s.split('.')))) diff --git a/handlers/main.yml b/handlers/main.yml new file mode 100644 index 0000000..2a2d3f6 --- /dev/null +++ b/handlers/main.yml @@ -0,0 +1,19 @@ +--- +- name: 'reload service files' + systemd: + daemon_reload: true + listen: gitea_reload_service_files + when: "ansible_facts['service_mgr'] == 'systemd'" + +- name: 'remove temporary directory' + file: + path: "{{ _tmpdir.path }}" + state: absent + diff: false + listen: 'gitea_remove_tmpdir' + +- name: 'restart gitea' + service: + name: 'gitea' + enabled: true + state: "{{ ansible_facts['is_chroot'] | ternary(omit, 'restarted') }}" diff --git a/library/gitea_auth.py b/library/gitea_auth.py new file mode 100755 index 0000000..e09d423 --- /dev/null +++ b/library/gitea_auth.py @@ -0,0 +1,547 @@ +#!/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() diff --git a/meta/main.yml b/meta/main.yml new file mode 100644 index 0000000..fbaea41 --- /dev/null +++ b/meta/main.yml @@ -0,0 +1,28 @@ +galaxy_info: + author: Sebastian Hamann + description: Install and configure Gitea + + license: MIT + + min_ansible_version: 2.8 + + platforms: + - name: Gentoo + versions: + - any + - name: Debian + versions: + - buster + - name: Alpine + versions: + - any + - name: GenericLinux + versions: + - any + + galaxy_tags: + - development + - git + - gitea + +dependencies: [] diff --git a/tasks/get_secrets.yml b/tasks/get_secrets.yml new file mode 100644 index 0000000..c0b82ca --- /dev/null +++ b/tasks/get_secrets.yml @@ -0,0 +1,25 @@ +--- +# This file extracts a secret from the config file. If it is not found, a new value is generated. +# In either case, the secret is made available in a variable for future reference. +# Needs: +# * _slurp_appini - base64-encoded contents of app.ini +# * secret - the name of the secret to handle + +- name: "extract {{ secret }} from current config file" + set_fact: + _secret_value: "{{ _slurp_appini.content | b64decode | regex_search('(?<=' ~ secret ~ '\\s=\\s`).*(?=`)') }}" + no_log: true + when: "_slurp_appini is not skipped" + +- name: "generate new {{ secret }}" + command: "gitea generate secret '{{ secret }}'" + environment: + PATH: "{{ ansible_facts['env']['PATH'] }}:/usr/local/bin" + register: _generate_secret + no_log: true + when: "not _secret_value | default(false)" + +- name: "store new {{ secret }}" + set_fact: + '_{{ secret }}': "{{ _generate_secret.stdout | default(_secret_value) }}" + no_log: true diff --git a/tasks/install_gitea_binary.yml b/tasks/install_gitea_binary.yml new file mode 100644 index 0000000..e36ece8 --- /dev/null +++ b/tasks/install_gitea_binary.yml @@ -0,0 +1,144 @@ +--- +# This file handles installing Gitea using the official binary release. +# This includes +# * installing dependencies using the package manager +# * getting the latest version +# * downloading the binary +# * validating the signature +# * installing the binary to the correct path +# * creating the necessary system user account +# * installing a systemd service file/OpenRC init script + +- name: 'install dependencies' + package: + name: "{{ gitea_dependencies }}" + state: present + +- block: + - name: 'get available versions of Gitea' + uri: + url: "{{ gitea_base_url }}" + return_content: true + register: _gitea_downloads + + - name: 'determine latest version of Gitea' + set_fact: + gitea_version: "{{ _gitea_downloads.content | regex_findall('(?<=/gitea/)[0-9]+\\.[0-9]+\\.[0-9]+') | sort_versions | last }}" + + when: "gitea_version is not defined" + +- name: 'get currently installed version of Gitea' + command: '/usr/local/bin/gitea --version' + check_mode: false + changed_when: false + ignore_errors: true + register: _gitea_version + +- name: 'determine version number' + set_fact: + _current_gitea_version: "{{ _gitea_version.stdout | default('') | regex_search('(?<=Gitea version )[0-9.]+(-rc[0-9]+)?(?= )') }}" + +- name: "download and install Gitea {{ gitea_version }}" + block: + + - name: 'create temporary directory' + tempfile: + state: directory + diff: false + register: _tmpdir + notify: 'gitea_remove_tmpdir' + + - name: 'determine file name' + set_fact: + _filename: "gitea-{{ gitea_version }}-linux-{{ gitea_arch }}" + + - name: 'download gitea' + get_url: + url: "{{ gitea_base_url | regex_replace('/*$', '') }}/{{ gitea_version }}/{{ filename }}" + dest: "{{ _tmpdir.path }}" + loop: + - "{{ _filename }}" + - "{{ _filename }}.asc" + loop_control: + loop_var: filename + + - name: 'create temporary GnuPG directory' + file: + path: "{{ _tmpdir.path }}/.gnupg" + state: directory + owner: root + group: root + mode: 0700 + diff: false + + - name: 'get the PGP keys' + command: "gpg --keyserver hkps://keys.openpgp.org --no-default-keyring --keyring trustedkeys.kbx --recv-key {{ gitea_pgp_fingerprint | quote }}" + environment: + GNUPGHOME: "{{ _tmpdir.path }}/.gnupg" + register: '_gpg_recv_key' + changed_when: "_gpg_recv_key is not failed and 'imported:' in _gpg_recv_key.stderr" + + - name: 'verify signature' + command: "gpgv '{{ _tmpdir.path }}/{{ _filename }}.asc' '{{ _tmpdir.path }}/{{ _filename }}'" + environment: + GNUPGHOME: "{{ _tmpdir.path }}/.gnupg" + changed_when: false + + - name: 'install gitea' + copy: + dest: '/usr/local/bin/gitea' + src: "{{ _tmpdir.path }}/{{ _filename }}" + remote_src: true + owner: root + group: root + mode: 0755 + notify: 'restart gitea' + + when: "gitea_version != _current_gitea_version" + +- name: 'create Gitea system user group' + group: + name: "{{ gitea_group }}" + system: true + state: present + +- name: 'create Gitea system user account' + user: + name: "{{ gitea_user }}" + password: '*' + group: "{{ gitea_group }}" + home: "{{ gitea_data_path }}" + comment: 'Gitea service account' + shell: '/bin/sh' + system: true + state: present + +- name: 'check for MariaDB (systemd)' + stat: + path: '/lib/systemd/system/mariadb.service' + register: _mariadb_service + when: "gitea_database_type == 'mysql' and ansible_facts['service_mgr'] == 'systemd'" + +- name: 'check for MariaDB (non-systemd)' + stat: + path: '/etc/init.d/mariadb' + register: _initd_mariadb + when: "gitea_database_type == 'mysql' and ansible_facts['service_mgr'] != 'systemd'" + +- name: 'install systemd service file' + template: + dest: '/etc/systemd/system/gitea.service' + src: 'gitea.service.j2' + owner: root + group: root + mode: 0644 + when: "ansible_facts['service_mgr'] == 'systemd'" + +- name: 'install OpenRC init script' + template: + dest: '/etc/init.d/gitea' + src: 'gitea.openrc.j2' + owner: root + group: root + mode: 0755 + when: "ansible_facts['service_mgr'] == 'openrc'" diff --git a/tasks/main.yml b/tasks/main.yml new file mode 100644 index 0000000..bd3b65d --- /dev/null +++ b/tasks/main.yml @@ -0,0 +1,266 @@ +--- +- name: 'gather os specific variables' + include_vars: "{{ vars_file }}" + loop: + - 'default.yml' + - "{{ ansible_facts['os_family'] | lower }}.yml" + - "{{ ansible_facts['distribution'] | lower }}.yml" + - "{{ ansible_facts['distribution'] | lower }}-{{ ansible_facts['distribution_major_version'] }}.yml" + - "{{ ansible_facts['distribution'] | lower }}-{{ ansible_facts['distribution_version'] }}.yml" + loop_control: + loop_var: vars_file + when: "(vars_file is abs and vars_file is file) or (vars_file is not abs and (role_path ~ '/vars/' ~ vars_file) is file)" + +- name: 'check for bash' + stat: + path: '/bin/bash' + register: _bin_bash + +- name: 'ensure configuration consistency' + set_fact: + gitea_use_pkg: "{{ gitea_has_pkg | bool and gitea_use_pkg | bool }}" + +- name: 'set user name to distribution package value' + set_fact: + gitea_user: "{{ gitea_pkg_user }}" + gitea_group: "{{ gitea_pkg_group }}" + when: "gitea_use_pkg | bool" + +- name: 'install Gitea using the package manager' + package: + name: "{{ gitea_pkg_name }}" + state: present + notify: 'restart gitea' + when: "gitea_use_pkg | bool" + +- name: 'install Gitea from binary' + include_tasks: install_gitea_binary.yml + when: "not gitea_use_pkg | bool" + +- name: 'install additional dependencies' + package: + name: "{{ pkg.pkg }}" + state: present + when: "pkg.when" + loop: + - pkg: 'git-lfs' + when: "{{ gitea_enable_lfs | bool }}" + - pkg: "{{ gitea_gnupg_package }}" + when: "{{ gitea_enable_signing | bool }}" + loop_control: + loop_var: 'pkg' + label: "{{ pkg.pkg }}" + +- name: "add {{ gitea_user }} to extra groups" + user: + name: "{{ gitea_user }}" + groups: "{{ gitea_extra_groups }}" + append: true + notify: 'restart gitea' + when: "[gitea_extra_groups | default([])] | flatten | count" + +- name: "harden gitea.service" + block: + - name: "create override directory for gitea.service" + file: + path: '/etc/systemd/system/gitea.service.d/' + state: directory + owner: root + group: root + mode: 0755 + + - name: "install override file for gitea.service" + template: + dest: '/etc/systemd/system/gitea.service.d/override.conf' + src: 'gitea_override.conf.j2' + owner: root + group: root + mode: 0644 + notify: + - 'gitea_reload_service_files' + - 'restart gitea' + + when: "ansible_facts['service_mgr'] == 'systemd'" + +- name: 'allow non-root users to bind to low ports' + sysctl: + name: 'net.ipv4.ip_unprivileged_port_start' + value: '0' + sysctl_file: '/etc/sysctl.d/unprivileged_ports.conf' + state: present + when: "ansible_facts['service_mgr'] != 'systemd' and gitea_port | int < 1024" + +- name: 'check if Gitea is already configured' + stat: + path: '/etc/gitea/app.ini' + register: _stat_appini + +- name: 'read current config file' + slurp: + src: '/etc/gitea/app.ini' + register: _slurp_appini + when: "_stat_appini.stat.exists" + +- include_tasks: get_secrets.yml + loop: + - 'SECRET_KEY' + - 'INTERNAL_TOKEN' + - 'JWT_SECRET' + - 'LFS_JWT_SECRET' + loop_control: + loop_var: secret + +- name: 'combine default and custom options' + set_fact: + _gitea_options: "{{ gitea_default_options | combine(gitea_extra_options, recursive=True) }}" + +- name: 'create required directories' + file: + path: "{{ directory.path }}" + state: directory + owner: "{{ directory.owner | default(gitea_user) }}" + group: "{{ directory.group | default(gitea_group) }}" + mode: "{{ directory.mode | default('0750') }}" + loop: + - path: '/etc/gitea' + owner: root + - path: "{{ gitea_data_path }}" + - path: "{{ _gitea_options['git']['HOME_PATH'] }}" + - path: "{{ gitea_custom_path }}" + owner: root + - path: "{{ gitea_log_path }}" + loop_control: + loop_var: directory + label: "{{ directory.path }}" + +- name: 'configure Gitea' + template: + dest: '/etc/gitea/app.ini' + src: 'app.ini.j2' + owner: root + group: "{{ gitea_group }}" + mode: 0640 + no_log: true + notify: 'restart gitea' + +- name: 'unset secrets' + set_fact: + _slurp_appini: + _secret_value: + _generate_secret: + _SECRET_KEY: + _INTERNAL_TOKEN: + _JWT_SECRET: + _LFS_JWT_SECRET: + +- name: 'create server-side commit signing key' + command: "su {{ gitea_user }} -c 'gpg --batch --generate-key'" + args: + warn: false # su is needed, otherwise Ansible might require a password to become the gitea user + creates: "{{ _gitea_options['git']['HOME_PATH'] }}/.gnupg/private-keys-v1.d/" + stdin: | + %no-protection + Key-Type: {{ gitea_signing_key_type }} + Key-Length: {{ gitea_signing_key_length }} + Key-Usage: sign + Name-Real: {{ gitea_committer_name }} + Name-Email: {{ gitea_committer_email }} + # Discard the time, use only the date as the creation timestamp + Creation-Date: {{ lookup('pipe', 'date +%Y-%m-%d') }} + when: "gitea_enable_signing | bool" + +- name: 'configure git command line client' + ini_file: + path: "{{ _gitea_options['git']['HOME_PATH'] }}/.gitconfig" + section: "{{ item.section }}" + option: "{{ item.option }}" + value: "{{ item.value }}" + state: present + loop: + - section: 'commit' + option: 'gpgsign' + value: "{{ gitea_enable_signing | bool | string | lower }}" + - section: 'user' + option: 'name' + value: "{{ gitea_committer_name }}" + - section: 'user' + option: 'email' + value: "{{ gitea_committer_email }}" + loop_control: + label: "{{ item.section }}.{{ item.option }} = {{ item.value }}" + +- name: 'initialise gitea database (this may take a long time)' + command: "su {{ gitea_user }} -c 'PATH=\"{{ ansible_facts['env']['PATH'] }}:/usr/local/bin\" gitea -c /etc/gitea/app.ini migrate'" + args: + chdir: "{{ gitea_data_path }}" + warn: false # su is needed, otherwise Ansible might require a password to become the gitea user + +- name: 'create initial local user accounts' + command: "su {{ gitea_user }} -c 'PATH=\"{{ ansible_facts['env']['PATH'] }}:/usr/local/bin\" gitea -c /etc/gitea/app.ini admin user create --username {{ user.name | quote }} --password {{ user.password | quote }} --email {{ user.email | quote }} {{ user.admin | default(false) | bool | ternary('--admin', '') }}'" + args: + chdir: "{{ gitea_data_path }}" + warn: false # su is needed, otherwise Ansible might require a password to become the gitea user + register: _create_user + failed_when: "_create_user.rc > 0 and 'user already exists' not in _create_user.stdout" + changed_when: "'New user ''' ~ user.name ~ ''' has been successfully created' in _create_user.stdout" + no_log: true + loop: "{{ gitea_users }}" + loop_control: + loop_var: user + label: "{{ user.name }}" + +- name: 'configure external authentication sources' + gitea_auth: + name: "{{ provider.name }}" + type: "{{ provider.type }}" + host: "{{ provider.host | default(omit) }}" + port: "{{ provider.port | default(omit) }}" + encryption: "{{ provider.encryption | default(omit) }}" + bind_dn: "{{ provider.bind_dn | default(omit) }}" + bind_password: "{{ provider.bind_password | default(omit) }}" + user_search_base: "{{ provider.user_search_base | default(omit) }}" + user_filter: "{{ provider.user_filter | default(omit) }}" + admin_filter: "{{ provider.admin_filter | default(omit) }}" + username_attribute: "{{ provider.username_attribute | default(omit) }}" + email_attribute: "{{ provider.email_attribute | default(omit) }}" + firstname_attribute: "{{ provider.firstname_attribute | default(omit) }}" + surname_attribute: "{{ provider.surname_attribute | default(omit) }}" + sshkey_attribute: "{{ provider.sshkey_attribute | default(omit) }}" + sync_users: "{{ provider.sync_users | default(omit) }}" + provider: "{{ provider.provider | default(omit) }}" + client_id: "{{ provider.client_id | default(omit) }}" + client_secret: "{{ provider.client_secret | default(omit) }}" + auto_discover_url: "{{ provider.auto_discover_url | default(omit) }}" + use_custom_urls: "{{ provider.use_custom_urls | default(omit) }}" + custom_tenant_id: "{{ provider.custom_tenant_id | default(omit) }}" + custom_auth_url: "{{ provider.custom_auth_url | default(omit) }}" + custom_email_url: "{{ provider.custom_email_url | default(omit) }}" + custom_profile_url: "{{ provider.custom_profile_url | default(omit) }}" + custom_token_url: "{{ provider.custom_token_url | default(omit) }}" + state: present + environment: + PATH: "{{ ansible_facts['env']['PATH'] }}:/usr/local/bin" + loop: "{{ gitea_auth_providers }}" + loop_control: + loop_var: provider + label: "{{ provider.name }}" + no_log: "{{ provider.bind_password is defined or provider.client_secret is defined }}" + +- name: 'install custom files' + copy: + src: "{{ gitea_custom_files }}/" + dest: "{{ gitea_custom_path }}" + owner: root + group: root + directory_mode: 0755 + when: "gitea_custom_files is defined" + notify: 'restart gitea' + +# If the unit file changed, reload it now. +- meta: flush_handlers + +- name: 'enable and start Gitea' + service: + name: 'gitea' + enabled: true + state: "{{ ansible_facts['is_chroot'] | ternary(omit, 'started') }}" diff --git a/templates/app.ini.j2 b/templates/app.ini.j2 new file mode 100644 index 0000000..bba7331 --- /dev/null +++ b/templates/app.ini.j2 @@ -0,0 +1,10 @@ +{# vim: filetype=ini.jinja2 #} +{{ ansible_managed | comment }} + +{% for key, options in _gitea_options.items() %} +[{{ key }}] +{% for k, v in options.items() if v != omit %} +{{ k }} = {{ v }} +{% endfor %} + +{% endfor %} diff --git a/templates/gitea.openrc.j2 b/templates/gitea.openrc.j2 new file mode 100644 index 0000000..bcb6f50 --- /dev/null +++ b/templates/gitea.openrc.j2 @@ -0,0 +1,45 @@ +{# vim: filetype=gentoo-init-d.jinja2 #} +#!/sbin/openrc-run + +name=gitea +description="Gitea, a self-hosted Git service" + +: ${GITEA_CONF:=/etc/gitea/app.ini} +: ${GITEA_USER:={{ gitea_user }}} +: ${GITEA_GROUP:={{ gitea_group }}} +: ${GITEA_WORK_DIR:={{ gitea_data_path }}} +: ${GITEA_CUSTOM:={{ gitea_custom_path }}} +: ${GITEA_LOG_FILE:={{ gitea_log_path }}/http.log} + +supervisor=supervise-daemon +command="/usr/local/bin/gitea" +command_args="--config '${GITEA_CONF}' web" +command_background="true" +command_user="${GITEA_USER}:${GITEA_GROUP}" +error_log="/var/log/${RC_SVCNAME}/${RC_SVCNAME}.err" +pidfile="/run/${RC_SVCNAME}.pid" +required_files="${GITEA_CONF}" +supervise_daemon_args="--env GITEA_WORK_DIR='${GITEA_WORK_DIR}' \ + --chdir '${GITEA_WORK_DIR}' \ + --stdout '${GITEA_LOG_FILE}' --stderr '${GITEA_LOG_FILE}'" + +depend() { + use logger dns + need net +{%- if gitea_database_host.startswith('127.') or gitea_database_host.startswith('localhost') or gitea_database_host.startswith('/') %} +{% if gitea_database_type == 'postgres' %} + postgresql +{% elif gitea_database_type == 'mysql' %} +{% if _initd_mariadb.stat.exists %} + mariadb +{% else %} + mysql +{% endif %} +{% else %} + +{% endif %} +{% else %} + +{% endif %} + after firewall +} diff --git a/templates/gitea.service.j2 b/templates/gitea.service.j2 new file mode 100644 index 0000000..344672c --- /dev/null +++ b/templates/gitea.service.j2 @@ -0,0 +1,32 @@ +{# vim: filetype=systemd.jinja2 #} +[Unit] +Description=Gitea (Git with a cup of tea) +After=network.target +Requires=network.target +{% if gitea_database_host.startswith('127.') or gitea_database_host.startswith('localhost') or gitea_database_host.startswith('/') %} +{% if gitea_database_type == 'postgres' %} +Requires=postgresql.service +{% elif gitea_database_type == 'mysql' %} +{% if _mariadb_service.stat.exists %} +Requires=mariadb.service +{% else %} +Requires=mysql.service +{% endif %} +{% endif %} +{% endif %} + +[Service] +Type=simple +Restart=always +RestartSec=2s + +User={{ gitea_user }} +Group={{ gitea_group }} + +ExecStart=/usr/local/bin/gitea web --config /etc/gitea/app.ini +WorkingDirectory={{ gitea_data_path | quote }} +RuntimeDirectory=gitea +Environment="GITEA_WORK_DIR={{ gitea_data_path }}" "GITEA_CUSTOM={{ gitea_custom_path }}" + +[Install] +WantedBy=multi-user.target diff --git a/templates/gitea_override.conf.j2 b/templates/gitea_override.conf.j2 new file mode 100644 index 0000000..5ec993e --- /dev/null +++ b/templates/gitea_override.conf.j2 @@ -0,0 +1,38 @@ +{# vim: filetype=systemd.jinja2 #} +[Service] +# Apply principle of least privilege +NoNewPrivileges=true +CapabilityBoundingSet= +{% if gitea_port | int < 1024 %} +# Allow binding to low ports +CapabilityBoundingSet=CAP_NET_BIND_SERVICE +AmbientCapabilities=CAP_NET_BIND_SERVICE +{% endif %} +ProtectSystem=strict +ProtectHome=true +ReadWritePaths={{ [gitea_data_path, gitea_log_path] | map('quote') | join(' ') }} +{% if gitea_inaccessible_paths | count %} +InaccessiblePaths={{ [gitea_inaccessible_paths] | flatten | unique | map('quote') | join(' ') }} +{% endif %} +PrivateTmp=true +PrivateDevices=true +{% if [gitea_extra_groups | default([])] | flatten | count == 0 %} +PrivateUsers=true +{% endif %} +ProtectKernelTunables=true +ProtectKernelModules=true +ProtectKernelLogs=true +ProtectControlGroups=true +ProtectProc=invisible +ProtectClock=true +ProtectHostname=true +LockPersonality=true +MemoryDenyWriteExecute=true +RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX +RestrictNamespaces=true +RestrictRealtime=true +RestrictSUIDSGID=true +SystemCallFilter=@system-service @basic-io @io-event @network-io madvise +SystemCallFilter=~@aio @chown @keyring @memlock @privileged @resources @setuid +SystemCallArchitectures=native +RemoveIPC=true diff --git a/vars/alpine.yml b/vars/alpine.yml new file mode 100644 index 0000000..a390100 --- /dev/null +++ b/vars/alpine.yml @@ -0,0 +1,8 @@ +--- +gitea_has_pkg: true +gitea_pkg_name: + - 'gitea' + - 'gitea-openrc' +gitea_pkg_user: 'gitea' +gitea_pkg_group: 'www-data' +gitea_gnupg_package: 'gnupg' diff --git a/vars/archlinux.yml b/vars/archlinux.yml new file mode 100644 index 0000000..a9eddfe --- /dev/null +++ b/vars/archlinux.yml @@ -0,0 +1,3 @@ +--- +gitea_has_pkg: true +gitea_gnupg_package: 'gnupg' diff --git a/vars/default.yml b/vars/default.yml new file mode 100644 index 0000000..aa9395f --- /dev/null +++ b/vars/default.yml @@ -0,0 +1,9 @@ +--- +gitea_has_pkg: false +gitea_pkg_name: 'gitea' +gitea_pkg_user: 'git' +gitea_pkg_group: "{{ gitea_pkg_user }}" +gitea_gnupg_package: 'gnupg2' +gitea_dependencies: + - 'git' + - "{{ gitea_gnupg_package }}" diff --git a/vars/gentoo.yml b/vars/gentoo.yml new file mode 100644 index 0000000..160422d --- /dev/null +++ b/vars/gentoo.yml @@ -0,0 +1,3 @@ +--- +gitea_has_pkg: true +gitea_gnupg_package: 'app-crypt/gnupg' diff --git a/vars/main.yml b/vars/main.yml new file mode 100644 index 0000000..9d9f62f --- /dev/null +++ b/vars/main.yml @@ -0,0 +1,74 @@ +--- +gitea_arch: "{{ 'amd64' if ansible_facts['architecture'] == 'x86_64' else ansible_facts['architecture'] }}" +gitea_base_url: 'https://dl.gitea.io/gitea/' +gitea_pgp_fingerprint: '7C9E68152594688862D62AF62D9AE806EC1592E2' +gitea_default_options: + DEFAULT: + RUN_USER: "{{ gitea_user }}" + RUN_MODE: 'prod' + repository: + ROOT: "{{ gitea_data_path }}/repos" + SCRIPT_TYPE: "{{ _bin_bash.stat.exists | ternary('bash', 'sh') }}" + git: + HOME_PATH: "{{ _gitea_options['server']['APP_DATA_PATH'] | default(gitea_data_path) ~ '/data/' }}/home" + repository.signing: + SIGNING_KEY: "{{ gitea_enable_signing | bool | ternary('default', 'none') }}" + server: + APP_DATA_PATH: "{{ gitea_data_path }}" + PROTOCOL: "{{ (gitea_tls_cert is defined) | ternary('https', 'http') }}" + DOMAIN: "{{ ansible_facts['fqdn'] }}" + HTTP_ADDR: "{{ gitea_bind_address }}" + HTTP_PORT: "{{ gitea_port }}" + SSH_LISTEN_HOST: "{{ gitea_bind_address }}" + OFFLINE_MODE: true + CERT_FILE: "{{ gitea_tls_cert | default(omit) }}" + KEY_FILE: "{{ gitea_tls_cert_key | default(omit) }}" + LFS_START_SERVER: "{{ gitea_enable_lfs }}" + LFS_JWT_SECRET: '`{{ _LFS_JWT_SECRET }}`' + database: + DB_TYPE: "{{ gitea_database_type | mandatory }}" + HOST: "{{ gitea_database_host | default(omit) }}" + NAME: "{{ (gitea_database_type != 'sqlite3') | ternary(gitea_database_name | default(omit), omit) }}" + USER: "{{ gitea_database_user | default(omit) }}" + PASSWD: "{{ '`' ~ gitea_database_password ~ '`' if gitea_database_password is defined else omit }}" + CHARSET: 'utf8mb4' + PATH: "{{ (gitea_database_type == 'sqlite3') | ternary(gitea_database_name | default(omit), omit) }}" + LOG_SQL: "{{ gitea_loglevel | lower in ['trace', 'debug'] }}" + indexer: + REPO_INDEXER_ENABLED: "{{ gitea_enable_indexer | bool }}" + security: + INSTALL_LOCK: true + SECRET_KEY: '`{{ _SECRET_KEY }}`' + INTERNAL_TOKEN: '`{{ _INTERNAL_TOKEN }}`' + COOKIE_USERNAME: 'gitea_user' + COOKIE_REMEMBER_NAME: 'gitea_persistent' + PASSWORD_HASH_ALGO: 'argon2' + service: + ENABLE_NOTIFY_MAIL: "{{ gitea_enable_mailer }}" + mailer: + ENABLED: "{{ gitea_enable_mailer }}" + SMTP_ADDR: "{% if ':' in gitea_mailer_host | default('') %}{{ gitea_mailer_host.split(':')[0] }}{% else %}{{ gitea_mailer_host | default(omit) }}{% endif %}" + SMTP_PORT: "{% if ':' in gitea_mailer_host | default('') %}{{ gitea_mailer_host.split(':')[1] }}{% else %}{{ omit }}{% endif %}" + FROM: "{{ gitea_mailer_from | default(omit) }}" + USER: "{{ gitea_mailer_user | default(omit) }}" + PASSWD: "{{ '`' ~ gitea_mailer_password ~ '`' if gitea_mailer_password is defined else omit }}" + SEND_AS_PLAIN_TEXT: true + PROTOCOL: "{% if gitea_mailer_host is defined %}{{ gitea_mailer_host.startswith('/') | ternary('smtp+unix', 'smtp') }}{% else %}sendmail{% endif %}" + session: + COOKIE_SECURE: "{{ gitea_tls_cert is defined }}" + COOKIE_NAME: 'gitea_session' + log: + ROOT_PATH: "{{ gitea_log_path }}" + # Default logger + MODE: 'file' + LEVEL: "{{ gitea_loglevel }}" + # Router logger + ROUTER: 'file' + ROUTER_LOG_LEVEL: "{{ gitea_loglevel }}" + # Access logger + ENABLE_ACCESS_LOG: "{{ gitea_loglevel | lower in ['trace', 'debug'] }}" + ACCESS: 'file' + oauth2: + JWT_SECRET: '`{{ _JWT_SECRET }}`' + other: + SHOW_FOOTER_VERSION: false