From 56e97040d6859279d5fbb7651522a5fa14b29062 Mon Sep 17 00:00:00 2001 From: Evgenii Alekseev Date: Fri, 5 Jan 2024 12:28:31 +0200 Subject: [PATCH] feat: read username if email is not available for oauth provider Also add recipe for OAuth with GitHub setup --- recipes/README.md | 1 + recipes/oauth/README.md | 15 +++++++ recipes/oauth/compose.yml | 58 +++++++++++++++++++++++++++ recipes/oauth/nginx.conf | 18 +++++++++ recipes/oauth/service.ini | 11 +++++ src/ahriman/core/auth/oauth.py | 2 +- tests/ahriman/core/auth/test_oauth.py | 11 +++++ 7 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 recipes/oauth/README.md create mode 100644 recipes/oauth/compose.yml create mode 100644 recipes/oauth/nginx.conf create mode 100644 recipes/oauth/service.ini diff --git a/recipes/README.md b/recipes/README.md index e49d5a0d..9d357985 100644 --- a/recipes/README.md +++ b/recipes/README.md @@ -10,6 +10,7 @@ Collection of the examples of docker compose configuration files, which covers s * [Distributed manual](distributed-manual): same as [distributed](distributed), but two nodes and update process must be run on worker node manually. * [i686](i686): non-x86_64 architecture setup. * [Multi repo](multirepo): run web service with two separated repositories. +* [OAuth](oauth): web service with OAuth (GitHub provider) authentication enabled. * [Pull](pull): normal service, but in addition with pulling packages from another source (e.g. GitHub repository). * [Sign](sign): create repository with database signing. * [Web](web): simple web service with authentication enabled. diff --git a/recipes/oauth/README.md b/recipes/oauth/README.md new file mode 100644 index 00000000..78138870 --- /dev/null +++ b/recipes/oauth/README.md @@ -0,0 +1,15 @@ +# OAuth + +1. Create user from `AHRIMAN_OAUTH_USER` environment variable (same as GitHub user). +2. Configure OAuth to use GitHub provider with client ID and secret specified in variables `AHRIMAN_OAUTH_CLIENT_ID` and `AHRIMAN_OAUTH_CLIENT_SECRET` variables respectively. +3. Setup repository named `ahriman-demo` with architecture `x86_64`. +4. Start web server at port `8080`. +5. Repository is available at `http://localhost:8080/repo`. + +Before you start, you need to create an application. It can be done by: + +1. Go to `https://github.com/settings/applications/new` +2. Set application name and its homepage. +3. Set callback url to `http://localhost:8080/api/v1/login` +4. Copy Client ID. +5. Generate new client secret and copy it. diff --git a/recipes/oauth/compose.yml b/recipes/oauth/compose.yml new file mode 100644 index 00000000..d62a37ea --- /dev/null +++ b/recipes/oauth/compose.yml @@ -0,0 +1,58 @@ +services: + backend: + image: arcan1s/ahriman:edge + privileged: true + + environment: + AHRIMAN_DEBUG: yes + AHRIMAN_OAUTH_CLIENT_ID: ${AHRIMAN_OAUTH_CLIENT_ID} + AHRIMAN_OAUTH_CLIENT_SECRET: ${AHRIMAN_OAUTH_CLIENT_SECRET} + AHRIMAN_OUTPUT: console + AHRIMAN_PORT: 8080 + AHRIMAN_PRESETUP_COMMAND: sudo -u ahriman ahriman user-add ${AHRIMAN_OAUTH_USER} -R full -p "" + AHRIMAN_REPOSITORY: ahriman-demo + AHRIMAN_UNIX_SOCKET: /var/lib/ahriman/ahriman/ahriman.sock + + configs: + - source: service + target: /etc/ahriman.ini.d/99-settings.ini + + volumes: + - type: volume + source: repository + target: /var/lib/ahriman + volume: + nocopy: true + + healthcheck: + test: curl --fail --silent --output /dev/null http://backend:8080/api/v1/info + interval: 10s + start_period: 30s + + command: web + + frontend: + image: nginx + ports: + - 8080:80 + + configs: + - source: nginx + target: /etc/nginx/conf.d/default.conf + + volumes: + - type: volume + source: repository + target: /srv + read_only: true + volume: + nocopy: true + +configs: + nginx: + file: nginx.conf + service: + file: service.ini + +volumes: + repository: diff --git a/recipes/oauth/nginx.conf b/recipes/oauth/nginx.conf new file mode 100644 index 00000000..fdd7195e --- /dev/null +++ b/recipes/oauth/nginx.conf @@ -0,0 +1,18 @@ +server { + listen 80; + + location /repo { + rewrite ^/repo/(.*) /$1 break; + autoindex on; + root /srv/ahriman/repository; + } + + location / { + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarder-Proto $scheme; + + proxy_pass http://backend:8080; + } +} diff --git a/recipes/oauth/service.ini b/recipes/oauth/service.ini new file mode 100644 index 00000000..96003669 --- /dev/null +++ b/recipes/oauth/service.ini @@ -0,0 +1,11 @@ +[auth] +target = oauth +client_id = $AHRIMAN_OAUTH_CLIENT_ID +client_secret = $AHRIMAN_OAUTH_CLIENT_SECRET + +oauth_icon = github +oauth_provider = GithubClient +oauth_scopes = read:user + +[web] +address = http://localhost:8080 diff --git a/src/ahriman/core/auth/oauth.py b/src/ahriman/core/auth/oauth.py index 3c67f913..763a5344 100644 --- a/src/ahriman/core/auth/oauth.py +++ b/src/ahriman/core/auth/oauth.py @@ -130,7 +130,7 @@ class OAuth(Mapping): client.access_token = access_token user, _ = await client.user_info() - username: str = user.email # type: ignore[attr-defined] + username: str = user.email or user.username # type: ignore[attr-defined] return username except Exception: self.logger.exception("got exception while performing request") diff --git a/tests/ahriman/core/auth/test_oauth.py b/tests/ahriman/core/auth/test_oauth.py index 7f8bd410..5575cccd 100644 --- a/tests/ahriman/core/auth/test_oauth.py +++ b/tests/ahriman/core/auth/test_oauth.py @@ -75,6 +75,17 @@ async def test_get_oauth_username(oauth: OAuth, mocker: MockerFixture) -> None: assert email == "email" +async def test_get_oauth_username_empty_email(oauth: OAuth, mocker: MockerFixture) -> None: + """ + must read username if email is not available + """ + mocker.patch("aioauth_client.GoogleClient.get_access_token", return_value=("token", "")) + mocker.patch("aioauth_client.GoogleClient.user_info", return_value=(aioauth_client.User(username="username"), "")) + + username = await oauth.get_oauth_username("code") + assert username == "username" + + async def test_get_oauth_username_exception_1(oauth: OAuth, mocker: MockerFixture) -> None: """ must return None in case of OAuth request error (get_access_token)