Skip to content

frontend/oidc: add OIDC_UNIQUE_PREFERRED_USERNAMES #3023

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Dec 19, 2023

Conversation

Bob131
Copy link
Contributor

@Bob131 Bob131 commented Nov 23, 2023

The current OpenID Connect code assumes the existence of a non-standard field in the UserInfo returned by the provider, limiting the ability to use non-Ipsilon OIDC providers; however, the standard forbids using corresponding standardised field (preferred_username) as a unique user identifier, a property on which Copr relies.

This commit adds a fall back path to the OIDC code that will use preferred_username anyway if the administrator asserts that the values will be unique in practice with a new configuration option. The upshot of this change is that administrators can now use OIDC providers like Gitea/Forgejo.

Fixes #3008.

@Bob131
Copy link
Contributor Author

Bob131 commented Nov 23, 2023

Testing this is kind of a pain in the neck. This is my attempt at a rough guide to reproduce my testing environment:

  1. Apply the following patch:
diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml
index d8eec3de..d55d3486 100644
--- a/docker-compose.dev.yaml
+++ b/docker-compose.dev.yaml
@@ -11,24 +11,24 @@ services:
   backend-log:
     environment:
       - PYTHONPATH=/opt/copr/backend
-    command: /usr/sbin/runuser -u copr -g copr -- /opt/copr/backend/run/copr_run_logger.py
+    command: /opt/copr/backend/run/copr_run_logger.py
 
   backend-build:
     environment:
       - PYTHONPATH=/opt/copr/backend
-    command: /usr/sbin/runuser -u copr -g copr -- /usr/bin/copr-run-dispatcher builds
+    command: /usr/bin/copr-run-dispatcher builds
 
   backend-action:
     environment:
       - PYTHONPATH=/opt/copr/backend
-    command: /usr/sbin/runuser -u copr -g copr -- /usr/bin/copr-run-dispatcher actions
+    command: /usr/bin/copr-run-dispatcher actions
 
   frontend:
     environment:
       - PYTHONPATH=/opt/copr/frontend/coprs_frontend
-    command: /usr/sbin/runuser -u copr-fe -g copr-fe -- /opt/copr/frontend/coprs_frontend/manage.py runserver -p 5000 -h 0.0.0.0 --without-threads --no-reload
+    command: sh -c 'while true; do /opt/copr/frontend/coprs_frontend/manage.py runserver -p 5000 -h 0.0.0.0 --without-threads --no-reload; done'
 
   distgit:
     environment:
       - PYTHONPATH=/opt/copr/dist-git
-    command: /usr/sbin/runuser -u copr-dist-git -g copr-dist-git -- /opt/copr/dist-git/run/importer_runner.py
+    command: /opt/copr/dist-git/run/importer_runner.py
diff --git a/docker-compose.forgejo.yaml b/docker-compose.forgejo.yaml
new file mode 100644
index 00000000..be543162
--- /dev/null
+++ b/docker-compose.forgejo.yaml
@@ -0,0 +1,12 @@
+version: '3'
+services:
+  forgejo:
+    image: codeberg.org/forgejo/forgejo:1.20.5-0
+    environment:
+      - USER_UID=1000
+      - USER_GID=1000
+    volumes:
+      - ./forgejo:/data
+    ports:
+      - '3000:3000'
+      - '2222:22'
diff --git a/frontend/coprs_frontend/coprs/oidc.py b/frontend/coprs_frontend/coprs/oidc.py
index d965186e..f22c054f 100644
--- a/frontend/coprs_frontend/coprs/oidc.py
+++ b/frontend/coprs_frontend/coprs/oidc.py
@@ -99,6 +99,7 @@ def init_oidc_app(app):
                 access_token_url=app.config.get("OIDC_TOKEN_URL"),
                 authorize_url=app.config.get("OIDC_AUTH_URL"),
                 userinfo_endpoint=app.config.get("OIDC_USERINFO_URL"),
+                jwks_uri=app.config.get("OIDC_JWKS_URL"),
                 client_kwargs=client_kwargs,
             )
     return oidc
  1. Start up Forgejo: podman compose -f docker-compose.yaml -f docker-compose.dev.yaml -f docker-compose.forgejo.yaml up forgejo
  2. Navigate to localhost:3000 and click through the setup UI
  3. Stop Forgejo and enable the OIDC provider by adding ENABLE = true under [oauth] in forgejo/gitea/conf/app.ini
  4. Start up everything: podman compose -f docker-compose.yaml -f docker-compose.dev.yaml -f docker-compose.forgejo.yaml up
  5. Create an OAuth application client and token from the Forgejo site administration UI. Add http://localhost:5000/oidc_auth/ to the redirect URI whitelist.
  6. Edit /etc/copr/copr.conf in copr_frontend_1 to include:
OIDC_LOGIN = True
OIDC_PROVIDER_NAME = "asdf" # e.g "openEuler ID"
OIDC_CLIENT = "<OID client ID from Forgejo>"
OIDC_SECRET = "<OID client secret from Forgejo>"
OIDC_SCOPES = "openid username profile email" # e.g. "openid username profile email"
OIDC_TOKEN_AUTH_METHOD="client_secret_post" # possible: client_secret_post, client_secret_basic, none

# The mixing of hostnames is intentional and required
OIDC_AUTH_URL="http://localhost:3000/login/oauth/authorize"
OIDC_TOKEN_URL="http://forgejo:3000/login/oauth/access_token"
OIDC_USERINFO_URL="http://forgejo:3000/login/oauth/userinfo"
OIDC_JWKS_URL="http://forgejo:3000/login/oauth/keys"
  1. Restart the frontend with podman exec -ti copr_frontend_1 bash and kill 2 3
  2. Try logging in. It should fail with an exception that hints towards a misconfiguration.
  3. Add OIDC_UNIQUE_PREFERRED_USERNAMES = True to copr.conf, restart the frontend and try again. It should work this time 🙂

@nikromen nikromen assigned nikromen and unassigned nikromen Nov 27, 2023
@praiskup
Copy link
Member

Testing this is kind of a pain in the neck. This is my attempt at a rough guide to reproduce my testing environment:

I think @FrostyX may comment on the docker-compose scripts if https://docs.pagure.org/copr.copr/contribute.html isn't helpful.

I must say the change is pretty straight-forward and non-intrusive, I like it.

@praiskup
Copy link
Member

NB, neither Fedora Copr nor Red Hat Copr (internal) have been using OIDC so far, it would be nice to let @pkking from openEuler comment on this PR

@praiskup
Copy link
Member

Is @ character part of the preferred_username?

Copy link
Member

@FrostyX FrostyX left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think @FrostyX may comment on the docker-compose scripts if https://docs.pagure.org/copr.copr/contribute.html isn't helpful.

Not sure if I can be of help here. I have no experience with Forgejo. When it comes to the Copr development, my workflow is kind of described here https://frostyx.cz/posts/copr-docker-compose-without-supervisord#running-services-from-git

@Bob131
Copy link
Contributor Author

Bob131 commented Dec 5, 2023

Is @ character part of the preferred_username?

I'm not sure what you mean. There were no @s in my testing, but the OIDC spec §5.1 says of preferred_username

This value MAY be any valid JSON string including special characters such as @, /, or whitespace

@pkking
Copy link
Contributor

pkking commented Dec 5, 2023

LGTM, since the behavior is opt-in :)

@Bob131 Bob131 force-pushed the oidc-unique-preferred-usernames branch from 53c6871 to a9dc527 Compare December 5, 2023 17:09
@Bob131
Copy link
Contributor Author

Bob131 commented Dec 5, 2023

This should address the suggestions that have been raised. With respect to @praiskup's suggested changes, I have kept username as the default rather than preferred_username because

  1. Defaulting to behaviour that the standard explicitly disallows seems a bit iffy, and
  2. I'm concerned about possibly breaking existing deployments

@Bob131 Bob131 force-pushed the oidc-unique-preferred-usernames branch from a9dc527 to bfdeb76 Compare December 5, 2023 17:19
@Bob131
Copy link
Contributor Author

Bob131 commented Dec 14, 2023

Having a look at the lint warnings, I'm not convinced that addressing them directly would improve the code; however, I'm unsure. Am I okay to just add lint suppression comments to squash those warnings?

@FrostyX
Copy link
Member

FrostyX commented Dec 14, 2023

Having a look at the lint warnings, I'm not convinced that addressing them directly would improve the code;

Error: PYLINT_WARNING:
coprs_frontend/coprs/oidc.py:16: R0911[too-many-return-statements]: oidc_enabled: Too many return statements (7/6)

There is a lot of repeated code like this

if not config.get("OIDC_CLIENT"):
    app.logger.error("OIDC_CLIENT is empty")
    return False

We could do something like (in a separate commit):

for key in ["OIDC_LOGIN", "OIDC_CLIENT", "OIDC_SECRET", "OIDC_SCOPES"]:
    if not config.get(key):
        app.logger.error("%s is empty", key)
        return False

but if you'd rather avoid changing the code, feel free to add pylint: disable statement.

Error: PYLINT_WARNING:
coprs_frontend/coprs/oidc.py:67:8: W0707[raise-missing-from]: oidc_username_from_userinfo: Consider explicitly re-raising using 'except KeyError as exc' and 'raise RuntimeError("Can't get unique username, see OIDC_USERNAME_CLAIM configuration docs") from exc'

This is easy to fix, we usually respect this warning.

@Bob131
Copy link
Contributor Author

Bob131 commented Dec 14, 2023

There is a lot of repeated code like this

if not config.get("OIDC_CLIENT"):
    app.logger.error("OIDC_CLIENT is empty")
    return False

We could do something like (in a separate commit):

for key in ["OIDC_LOGIN", "OIDC_CLIENT", "OIDC_SECRET", "OIDC_SCOPES"]:
    if not config.get(key):
        app.logger.error("%s is empty", key)
        return False

Ah, that makes sense. I can do that.

Error: PYLINT_WARNING:
coprs_frontend/coprs/oidc.py:67:8: W0707[raise-missing-from]: oidc_username_from_userinfo: Consider explicitly re-raising using 'except KeyError as exc' and 'raise RuntimeError("Can't get unique username, see OIDC_USERNAME_CLAIM configuration docs") from exc'

This is easy to fix, we usually respect this warning.

Ack. Glad I asked 🙂

A small snippet of code checking the existence of a particular key is
duplicated several times in oidc_enabled().  This commit is a small
clean up to reduce some of the duplicated code.
The current OpenID Connect code assumes the existence of a
non-standard field in the UserInfo returned by the provider, limiting
the ability to use non-Ipsilon OIDC providers; however, the
standard forbids using corresponding standardised field
(preferred_username) as a unique user identifier, a property on which
Copr relies.

This commit allows the administrator to specify which claim from
UserInfo should be used as a username; they may, for instance, choose
preferred_username if they know that the values from their OIDC
provider will be unique.  The upshot of this change is that
administrators can now use OIDC providers like Gitea/Forgejo.

Fixes fedora-copr#3008.
@Bob131 Bob131 force-pushed the oidc-unique-preferred-usernames branch from bfdeb76 to e609005 Compare December 14, 2023 14:58
Copy link
Member

@FrostyX FrostyX left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you very much for the changes.

@FrostyX FrostyX merged commit bcf645d into fedora-copr:main Dec 19, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Copr uses non-standard username field of OIDC UserInfo
5 participants