Skip to content

Commit

Permalink
OIDC authentication
Browse files Browse the repository at this point in the history
Signed-off-by: Aurélien Bompard <aurelien@bompard.org>
  • Loading branch information
abompard committed Aug 12, 2024
1 parent 26a6995 commit a393e0c
Show file tree
Hide file tree
Showing 25 changed files with 274 additions and 99 deletions.
4 changes: 4 additions & 0 deletions devel/ansible/playbook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,8 @@
- ipa-client
- cert
- gss-proxy
- name: oidc-register
redirect_url: https://{{ ansible_fqdn }}/docs/oauth2-redirect
public_client: true
dest: /home/vagrant/frontend_oidc.json
- dev
36 changes: 35 additions & 1 deletion devel/ansible/roles/dev/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,42 @@
args:
chdir: /home/vagrant/webhook-to-fedora-messaging

# Add Tinystage's root CA to the CA bundle
- name: Get the content of the CA cert
slurp:
src: /etc/ipa/ca.crt
register: ca_crt
- name: Find where certifi's CA bundle is located
command:
cmd: poetry run python -c "import certifi; print(certifi.where())"
chdir: /home/vagrant/webhook-to-fedora-messaging/
become: true
become_user: vagrant
register: _ca_bundle_path
changed_when: False
- name: Put tinystage root CA in the list of CAs
blockinfile:
block: "{{ ca_crt.content | b64decode }}"
path: "/etc/pki/tls/certs/ca-bundle.crt"
- name: Put tinystage root CA in the list of CA's for certifi
blockinfile:
block: "{{ ca_crt.content | b64decode }}"
path: "{{ _ca_bundle_path.stdout }}"
become: true
become_user: vagrant

# Authentication
- name: get the OIDC config
ansible.builtin.slurp:
src: /home/vagrant/frontend_oidc.json
register: oidc_config

- name: extract the OIDC client_id
set_fact:
oidc_client_id: "{{ oidc_config['content'] | b64decode | from_json | json_query('web.client_id') }}"

- name: copy the config file
copy:
template:
src: w2fm.cfg
dest: /home/vagrant/w2fm.cfg
mode: 0644
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
FASJSON_URL = "https://fasjson.tinystage.test/fasjson"
DATABASE__SQLALCHEMY__URL = "sqlite:////home/vagrant/w2fm.db"
LOGGING_CONFIG = "/home/vagrant/logging.yaml"
OIDC__PROVIDER_URL = "https://ipsilon.tinystage.test/idp/openidc"
OIDC__CLIENT_ID = "{{ oidc_client_id }}"
5 changes: 5 additions & 0 deletions devel/ansible/roles/oidc-register/defaults/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
ipsilon_hostname: ipsilon.tinystage.test
redirect_path: oidc/authorize
redirect_url: https://{{ ansible_fqdn }}/{{ redirect_path }}
dest: /home/vagrant/client_secrets.json
public_client: false
27 changes: 27 additions & 0 deletions devel/ansible/roles/oidc-register/tasks/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
- name: Install RPM packages
dnf:
name:
- python3-pip
- python3-devel
state: present

- name: install oidc_register with pip
pip:
name: oidc-register

# not sure of how to do this another way, but without this, oidc_register fails
- name: Get the content of the CA cert
slurp:
src: /etc/ipa/ca.crt
register: ca_crt
- name: Put tinystage root CA in the list of CA's for httplib2
blockinfile:
block: "{{ ca_crt.content | b64decode }}"
path: /usr/local/lib/python{{ ansible_local["python"]["py3"]["version"] }}/site-packages/httplib2/cacerts.txt

- name: register the application with oidc-register
shell:
cmd: oidc-register --debug {% if public_client %}--public-client{% endif %} --output-file {{ dest }} https://{{ ipsilon_hostname }}/idp/openidc/ {{ redirect_url }}
creates: "{{ dest }}"
become: yes
become_user: vagrant
27 changes: 26 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ asyncpg = {version = "^0.29.0", optional = true}
uvicorn = {extras = ["standard"], version = "^0.30.5"}
pydantic-settings = "^2.4.0"
aiosqlite = "^0.20.0"
authlib = "^1.3.1"
itsdangerous = "^2.2.0"


[tool.poetry.group.dev.dependencies]
Expand Down
18 changes: 14 additions & 4 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
# SPDX-License-Identifier: GPL-3.0-or-later

from typing import AsyncGenerator
from unittest import mock

import pytest
from httpx import AsyncClient, BasicAuth
from httpx import ASGITransport, AsyncClient

from webhook_to_fedora_messaging import database
from webhook_to_fedora_messaging.config import set_config_file
Expand Down Expand Up @@ -47,7 +48,8 @@ async def db_session(db):
@pytest.fixture()
async def client(app_config, db):
app = create_app()
async with AsyncClient(app=app, base_url="http://test") as client:
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
yield client


Expand All @@ -69,8 +71,16 @@ async def db_user(client, db_session) -> AsyncGenerator[User, None]:


@pytest.fixture()
async def client_auth(db_user):
return BasicAuth(username=db_user.name, password=db_user.name)
async def authenticated(db_user, client):
oidc_user = {
"nickname": db_user.name,
"email": f"{db_user.name}@example.com",
"sub": "dummyusersub",
}
with mock.patch("webhook_to_fedora_messaging.auth.oauth") as oauth:
oauth.fedora.userinfo = mock.AsyncMock(return_value=oidc_user)
client.headers = {"Authorization": "Bearer dummy-token"}
yield oauth


@pytest.fixture()
Expand Down
6 changes: 3 additions & 3 deletions tests/test_message/test_message_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ async def test_message_create(client, db_service, request_data, request_headers,
)
with mock_sends(GithubMessageV1) as sent_msgs:
response = await client.post(
f"/api/v1/messages/{db_service.uuid}", data=request_data, headers=request_headers
f"/api/v1/messages/{db_service.uuid}", content=request_data, headers=request_headers
)
assert response.status_code == 202, response.text
sent_msg = sent_msgs[0]
Expand All @@ -67,7 +67,7 @@ async def test_message_create(client, db_service, request_data, request_headers,
async def test_message_create_400(client, db_service, request_data, request_headers):
request_headers["X-Hub-Signature-256"] = ""
response = await client.post(
f"/api/v1/messages/{db_service.uuid}", data=request_data, headers=request_headers
f"/api/v1/messages/{db_service.uuid}", content=request_data, headers=request_headers
)
assert response.status_code == 400

Expand All @@ -78,5 +78,5 @@ async def test_message_create_404(client):


async def test_message_create_bad_request(client, db_service):
response = await client.post(f"/api/v1/messages/{db_service.uuid}", data="not json")
response = await client.post(f"/api/v1/messages/{db_service.uuid}", content="not json")
assert response.status_code == 422
8 changes: 4 additions & 4 deletions tests/test_service/test_service_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
],
)
@pytest.mark.usefixtures("db_user")
async def test_service_create(client, client_auth, data, code):
response = await client.post("/api/v1/services", auth=client_auth, json={"data": data})
async def test_service_create(client, authenticated, data, code):
response = await client.post("/api/v1/services", json={"data": data})
assert response.status_code == code, response.text
if code == 201:
result = response.json()
Expand All @@ -27,12 +27,12 @@ async def test_service_create(client, client_auth, data, code):
assert result["data"][prop] == data[prop]


async def test_service_conflict(client, client_auth, db_service, db_user):
async def test_service_conflict(client, authenticated, db_service, db_user):
data = {
"name": db_service.name,
"type": db_service.type,
"desc": db_service.desc,
}

response = await client.post("/api/v1/services", auth=client_auth, json={"data": data})
response = await client.post("/api/v1/services", json={"data": data})
assert response.status_code == 409
10 changes: 5 additions & 5 deletions tests/test_service/test_service_get.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
async def test_service_lookup(client, client_auth, db_service):
response = await client.get(f"/api/v1/services/{db_service.uuid}", auth=client_auth)
assert response.status_code == 200
async def test_service_lookup(client, authenticated, db_service):
response = await client.get(f"/api/v1/services/{db_service.uuid}")
assert response.status_code == 200, response.text
assert response.json() == {
"data": {
"creation_date": db_service.creation_date.isoformat(),
Expand All @@ -14,6 +14,6 @@ async def test_service_lookup(client, client_auth, db_service):
}


async def test_service_404(client, client_auth):
response = await client.get("/api/v1/services/not-existent-uuid", auth=client_auth)
async def test_service_404(client, authenticated):
response = await client.get("/api/v1/services/not-existent-uuid")
assert response.status_code == 404
4 changes: 2 additions & 2 deletions tests/test_service/test_service_list.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
async def test_service_list(client, client_auth, db_service):
response = await client.get("/api/v1/services", auth=client_auth)
async def test_service_list(client, authenticated, db_service):
response = await client.get("/api/v1/services")
assert response.status_code == 200
assert response.json() == {
"data": [
Expand Down
12 changes: 4 additions & 8 deletions tests/test_service/test_service_refresh.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
async def test_service_refresh(client, client_auth, db_service):
async def test_service_refresh(client, authenticated, db_service):
data = {"service_uuid": db_service.uuid}
response = await client.put(
f"/api/v1/services/{db_service.uuid}/regenerate", auth=client_auth, json=data
)
response = await client.put(f"/api/v1/services/{db_service.uuid}/regenerate", json=data)
assert response.status_code == 202


async def test_service_refresh_404(client, client_auth):
async def test_service_refresh_404(client, authenticated):
data = {"service_uuid": "not-existent-uuid"}
response = await client.put(
"/api/v1/services/not-existent-uuid/regenerate", auth=client_auth, json=data
)
response = await client.put("/api/v1/services/not-existent-uuid/regenerate", json=data)
assert response.status_code == 404
8 changes: 4 additions & 4 deletions tests/test_service/test_service_revoke.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
async def test_service_revoke(client, client_auth, db_service):
response = await client.put(f"/api/v1/services/{db_service.uuid}/revoke", auth=client_auth)
async def test_service_revoke(client, authenticated, db_service):
response = await client.put(f"/api/v1/services/{db_service.uuid}/revoke")
assert response.status_code == 202


async def test_service_revoke_404(client, client_auth, db_service):
response = await client.put("/api/v1/services/non-existent-uuid/revoke", auth=client_auth)
async def test_service_revoke_404(client, authenticated, db_service):
response = await client.put("/api/v1/services/non-existent-uuid/revoke")
assert response.status_code == 404
18 changes: 6 additions & 12 deletions tests/test_service/test_service_update.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,19 @@
async def test_service_update(client, client_auth, db_service, db_session):
async def test_service_update(client, authenticated, db_service, db_session):
data = {"name": "new name"}
response = await client.put(
f"/api/v1/services/{db_service.uuid}", auth=client_auth, json={"data": data}
)
response = await client.put(f"/api/v1/services/{db_service.uuid}", json={"data": data})
assert response.status_code == 202, response.text
assert response.json()["data"]["name"] == "new name"
await db_session.refresh(db_service)
assert db_service.name == "new name"


async def test_service_update_404(client, client_auth):
async def test_service_update_404(client, authenticated):
data = {"name": "new name"}
response = await client.put(
"/api/v1/services/non-existent-uuid", auth=client_auth, json={"data": data}
)
response = await client.put("/api/v1/services/non-existent-uuid", json={"data": data})
assert response.status_code == 404


async def test_service_update_bad_request(client, client_auth, db_service):
async def test_service_update_bad_request(client, authenticated, db_service):
data = {"something-else": "extra attr"}
response = await client.put(
f"/api/v1/services/{db_service.uuid}", auth=client_auth, json={"data": data}
)
response = await client.put(f"/api/v1/services/{db_service.uuid}", json={"data": data})
assert response.status_code == 422
4 changes: 2 additions & 2 deletions tests/test_user/test_user_get.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
),
],
)
async def test_user_get(client, client_auth, db_user, username, code):
response = await client.get(f"/api/v1/users/{username}", auth=client_auth)
async def test_user_get(client, authenticated, db_user, username, code):
response = await client.get(f"/api/v1/users/{username}")
assert response.status_code == code
if code == 200:
assert response.json() == {
Expand Down
4 changes: 2 additions & 2 deletions tests/test_user/test_user_search.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
async def test_user_search(client, client_auth, db_user):
response = await client.get("/api/v1/users/search/met", auth=client_auth)
async def test_user_search(client, db_user):
response = await client.get("/api/v1/users/search/met")
assert response.status_code == 200
assert response.json() == {
"data": [
Expand Down
Loading

0 comments on commit a393e0c

Please sign in to comment.