OAuth2 provider provisioning

hey there,

I could not find anything in the documentation, but maybe I am just being blind…

Is there any possibility to provision gitea as an oauth2 provider directly? I am already using the command line interface (in ansible) for adding LDAP authentication, now I would like to add an oauth2 app (drone), too.

I can do this manually but it would be nice if it is possible to automate this last step, too. Drone itself could be provisioned easily when I already had the client ID and secret from gitea…

Thanks in advance

You can do this with the API! I’ve deployed gitea + drone with ansible. The code I came up with is kind of tangled because talking to APIs in ansible is tangled, and because it must create a dummy user, but it works.

Here it is:

- name: DroneCI server
  # This play talks to both Gitea and Drone to connect them to each other
  hosts:
    - drone.data.neuropoly.org
    - drone.data.dev.neuropoly.org

  vars:
    drone_gitea_server: '{{ ansible_nodename | replace("drone.","",1) }}'
    ci_admin_user: ci-admin

  tasks:
    # Gitea insists on hanging all OAuth apps off specific user accounts
    # so we need a dummy account to hold Drone's credentials, and we need
    # to know its password. The easiest way to knowing is resetting.
    # This means this play is not 100% idempotent, but this password is not meant to
    # be known by anyone except ansible; the site admins can reset it for themselves
    # from the web UI if they really need it.
    - name: 'Generate Gitea {{ci_admin_user}}''s password'
      set_fact:
        # ref: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/password_lookup.html#examples
        ci_admin_password: "{{ lookup('password', '/dev/null') }}"
      no_log: true

    - name: 'Create/reset Gitea {{ci_admin_user}}''s account/password'
      delegate_to: '{{drone_gitea_server}}'
      shell: |
        # `admin user list` doesn't have a way to query specific users, so we've gotta filter it ourselves.
        # grep by itself could get confused by a different user with "{{ci_admin_user}}" in their email address
        # so we use awk to target accurately, then `| grep ''` (meaning`!= ""`) to convert it to a simple exit code
        # this `if` is here to make this idempotent
        if /srv/gitea/gitea admin user list | awk '$2=="{{ci_admin_user}}"' | grep '' >/dev/null; then
          # dummy user exists, reset its password
          /srv/gitea/gitea admin user change-password \
            --username {{ci_admin_user}} \
            --password {{ci_admin_password}}
        else
          # dummy user does not exist, create it
          /srv/gitea/gitea admin user create \
            --username {{ci_admin_user}} \
            --email {{ci_admin_user}}@{{drone_gitea_server}} \
            --password {{ci_admin_password}} \
            --must-change-password=false
        fi
      become: yes
      become_user: 'gitea'
      register: ci_owner
      changed_when: ci_owner.stdout | regex_search("New user") # only count creation as a change; resetting the password isn't a real change

    - name: Upload Drone Logo
      delegate_to: '{{drone_gitea_server}}'
      file:
        path: /srv/gitea/data/avatars/
        state: directory
        recurse: yes
        owner: gitea
        group: gitea

    - name: Upload Drone Logo
      delegate_to: '{{drone_gitea_server}}'
      copy:
        # TODO: make this whole script into a role and put this file in a proper subpath in it
        src: drone-logo-vector-dark.png
        dest: /srv/gitea/data/avatars/drone-logo-vector-dark.png
        owner: gitea
        group: gitea

    - name: "Set Gitea {{ci_admin_user}} profile page"
      # It's weird that we can't just, but since we have to have a user
      # make sure users can read an explanation for it inline
      uri:
        method: PATCH
        url: 'https://{{drone_gitea_server}}/api/v1/user/settings'
        force_basic_auth: yes
        user: '{{ci_admin_user}}'
        password: '{{ci_admin_password}}'
        body_format: json
        body:
          description: "Manages the connection to https://{{inventory_hostname}}"
          full_name: "DroneCI Administrator"
      no_log: true

    - name: "Set Gitea {{ci_admin_user}} profile page (avatar)"
      delegate_to: '{{drone_gitea_server}}'
      # /user/settings straight up ignores avatar_url
      # so circumvent it by going into the database
      command: |
        /usr/bin/psql -c "update public.user set avatar = 'drone-logo-vector-dark.png' where name = '{{ci_admin_user}}'"
      become: yes
      become_user: gitea
      register: psql_output
      changed_when: false

    - name: 'Check for OAuth credentials'
      uri:
        method: GET
        url: 'https://{{drone_gitea_server}}/api/v1/user/applications/oauth2'
        force_basic_auth: yes
        user: '{{ci_admin_user}}'
        password: '{{ci_admin_password}}'
      no_log: true
      register: gitea_oauth

    - set_fact:
        oauth_app_id: "{{ (gitea_oauth.json | selectattr('name', 'equalto', 'DroneCI') | map(attribute='id') | first) if (gitea_oauth.json|length>0) else None }}"

    - name: Create OAuth credentials for DroneCI via /user/applications/oauth2 API
      uri:
        # As with '{{ci_admin_user}}''s password,
        # the best -- in this case only -- way to know the OAuth
        # credentials are to reset/create them, so this also
        # makes this script non-idempotent, but only a little.

        # If the app already exists, use 'PATCH' to reset it,
        # but if not use 'POST' to create it.
        method: "{{ 'PATCH' if oauth_app_id else 'POST' }}"
        url: "https://{{drone_gitea_server}}/api/v1/user/applications/oauth2{{ '/' if oauth_app_id else ''}}{{oauth_app_id}}"
        #headers:
        #  Authorization: 'Bearer {{ci_admin_token}}'
        force_basic_auth: yes
        user: '{{ci_admin_user}}'
        password: '{{ci_admin_password}}'
        body_format: json
        body:
          name: "DroneCI"
          redirect_uris:
            # Drone's redirect_uri is *always* /login (without any trailing anything)
            # NB: {{inventory_hostname}} means Drone; it's not affected by the delegate_to:
            - "https://{{inventory_hostname}}/login"
        status_code: [200, 201] # POST returns 201 Created on success
      #delegate_to: localhost # conflicts with -e ansible_become=yes -e ansible_user=ubuntu
      no_log: true
      register: gitea_ci_oauth

I then have a separate self-contained role that I am writing to deploy Drone, which I feed the OAuth2 credentials into:

    - include_role:
        name: neuropoly.droneci
      no_log: true
      vars:
        drone_admin_user: "{{ci_admin_user}}" # reuse the Gitea admin account name for the Drone admin account name to hopefully keep things simple
        drone_oauth_id: "{{ gitea_ci_oauth.json.client_id }}"
        drone_oauth_secret: "{{ gitea_ci_oauth.json.client_secret }}"
        drone_runner_capacity: '{{ansible_processor_cores}}'

I plan to publish this all to Ansible Galaxy at some point, but it’s still a work in progress. There might already be something on Ansible Galaxy that I’ve missed, or maybe there will be by the time I feel ready to sshare.