redmine_oauth icon indicating copy to clipboard operation
redmine_oauth copied to clipboard

Require a certain role or group membership to allow login

Open col-panic opened this issue 1 year ago • 2 comments

Optionally require a certain role or group membership to allow login. (e.g. the user must have the client role redmine-user to be allowed login in the system)

col-panic avatar May 17 '24 06:05 col-panic

I need that as well. The only thing I'm missing is role mapping after login.

ok2uec avatar Aug 15 '24 06:08 ok2uec

What OAuth provider do you use? In Azure it should be possible in general. If you specify groupMembershipClaims, then we should get the access token with a list of groups. If you have the option to set the groupMembershipClaims in your application, I can create a branch where I log the access token to see whether some groups are present and in in format. I can't test it myself as I have no direct access to Azure administration. https://stackoverflow.com/questions/54868974/getting-security-groups-in-jwt-access-token

picman avatar Aug 16 '24 07:08 picman

We have a Keycloak Realm with multiple users. Now I only want a subset of this users to be able to login to Redmine. Keyloak is an Authentication tool, while the task of authorization is done by the Resource Provider (in this case Redmine). At the moment redmine_oauth does only require a user to successfully authenticate against a realm to be able to login as user. This already leaves projects marked as "public" open to this user.

(There exists an extension to block a user from getting a client token in https://github.com/sventorben/keycloak-restrict-client-auth - but as mentioned, this is considered to be a task of the resource provider)

The basic approach would be, that a client role user is introduced, and if the token represented by the user does not contain a role like resource_access.redmine.user then access is not granted.

In the following IDToken I added myself to a newly required client role user

{
  "exp": 1724998286,
  "iat": 1724997986,
  "jti": "fb1d6fb5-1734-4c16-ab98-4e008b9ffefd",
  "iss": "https://***/realms/***",
  "aud": "redmine",
  "sub": "8e82f27a-39cf-4540-a308-ddd8c94e6a5d",
  "typ": "ID",
  "azp": "redmine",
  "sid": "dddeb1de-ebd8-4c6f-913e-290ae5d5f8ac",
  "acr": "1",
  "resource_access": {
    "redmine": {
      "roles": [
        "user"
      ]
    }
  },
  "email_verified": true,
  "name": "Marco Descher",
  "preferred_username": "mdescher",
  "given_name": "Marco",
  "family_name": "Descher",
  "email": "***@***"
}

the mapping to resource_access.redmine.roles is keycloaks default to map a users client.role. The location should however be configurable. This role could also be mapped to the access token. I asumme, however, that you are reading the IDToken.

So the following extensions could be realize:

  1. Required the client role user to be authorized to login to redmine. (This feature could be optional. If someone has a setup where having a valid user on the openid server is already enough, then we would not need to check this)
  2. EXTENSION Connection to https://github.com/kontron/redmine_oauth/issues/37 if e.g. the token has a value like this
"resource_access": {
    "redmine": {
      "roles": [
        "user", "admin"
      ]
    }
  }

then the user could already be granted the Administrator privileges. Some way round - if admin is not part of the token anymore, then the admin rights should be removed. 3. EXTENSION Introducing groups which could then contain memberships to groups as used within redmine. But I don't elaborate on this here. If we come to this point, I'll create a separate issue explaining in detail.

col-panic avatar Aug 30 '24 06:08 col-panic

Could you test roles branch with your Keycloak configuration?

picman avatar Sep 05 '24 06:09 picman

@picman thank you for your response, I added some comments to https://github.com/kontron/redmine_oauth/commit/e1f6a6808e5366e5f84c05c20e94388270c94b27

col-panic avatar Sep 05 '24 07:09 col-panic

Updated accordingly.

picman avatar Sep 05 '24 09:09 picman

Validate user roles = resource_access.mis.roles

If none of the roles is mapped, then this happens:

mis-echo-1  | OAuth2::AccessToken.from_hash: `hash` contained more than one 'token' key (["access_token", "id_token"]); using "access_token".
mis-echo-1  | E, [2024-09-05T13:23:35.291165 #1] ERROR -- : undefined method `[]' for nil:NilClass

it is correct not to let me in - but there should be a better message :)

if user role is mapped the same happens for the access token

{
  "exp": 1725543182,
  "iat": 1725542882,
  "jti": "433d0ae4-3821-499c-9eee-06f061685acd",
  "iss": "https://keycloak.medelexis.ch/realms/Medelexis",
  "sub": "c64712b8-a25b-4368-82e0-a73d5fb7c944",
  "typ": "Bearer",
  "azp": "mis",
  "sid": "b9957d40-8c9e-4881-ba03-92eb636de2eb",
  "acr": "1",
  "allowed-origins": [
    "/*"
  ],
  "resource_access": {
    "mis": {
      "roles": [
        "user"
      ]
    }
  },
  "scope": "openid profile email",
  "email_verified": true,
  "name": "Test flawil",
  "preferred_username": "tester",
  "locale": "de",
  "given_name": "Test",
  "family_name": "flawil",
  "email": "[email protected]"
}

col-panic avatar Sep 05 '24 13:09 col-panic

  1. How does look the token if "none of the roles is mapped"?
  2. What do you mean with "the same happens for the access token"?

picman avatar Sep 05 '24 13:09 picman

  • Keycloak seems to deliver both tokens. I think you should focus for the id_token currently it seems to select the access_token
  • With no role granted at all, the id_token looks like this
{
  "exp": 1725547546,
  "iat": 1725547246,
  "jti": "1e939b4e-9df2-412d-89a7-89b77e5e9363",
  "iss": "https://keycloak.medelexis.ch/realms/Medelexis",
  "aud": "mis",
  "sub": "c64712b8-a25b-4368-82e0-a73d5fb7c944",
  "typ": "ID",
  "azp": "mis",
  "sid": "a83e1b4e-c707-4a8b-b14a-ee806bf35708",
  "acr": "1",
  "email_verified": true,
  "name": "Test flawil",
  "preferred_username": "tester",
  "locale": "de",
  "given_name": "Test",
  "family_name": "flawil",
  "email": "[email protected]"
}

the access-token like this

{
  "exp": 1725547546,
  "iat": 1725547246,
  "jti": "c201968e-4c78-4124-a244-5b37e1ea6064",
  "iss": "https://keycloak.medelexis.ch/realms/Medelexis",
  "sub": "c64712b8-a25b-4368-82e0-a73d5fb7c944",
  "typ": "Bearer",
  "azp": "mis",
  "sid": "7efbb610-e786-43c4-aaee-9cc02dc1f02a",
  "acr": "1",
  "allowed-origins": [
    "/*"
  ],
  "scope": "openid profile email",
  "email_verified": true,
  "name": "Test flawil",
  "preferred_username": "tester",
  "locale": "de",
  "given_name": "Test",
  "family_name": "flawil",
  "email": "[email protected]"
}
  • With the role user granted for the client, the id_token looks like this
{
  "exp": 1725547853,
  "iat": 1725547553,
  "jti": "99f42891-d424-4bc7-b403-3a41967ef884",
  "iss": "https://keycloak.medelexis.ch/realms/Medelexis",
  "aud": "mis",
  "sub": "c64712b8-a25b-4368-82e0-a73d5fb7c944",
  "typ": "ID",
  "azp": "mis",
  "sid": "ae6b6e47-a4f3-4d35-841b-27bdc395119b",
  "acr": "1",
  "resource_access": {
    "mis": {
      "roles": [
        "user"
      ]
    }
  },
  "email_verified": true,
  "name": "Test flawil",
  "preferred_username": "tester",
  "locale": "de",
  "given_name": "Test",
  "family_name": "flawil",
  "email": "[email protected]"
}

the access_token like this

{
  "exp": 1725547853,
  "iat": 1725547553,
  "jti": "107e0d71-7bb8-4de0-b759-85245ace0623",
  "iss": "https://keycloak.medelexis.ch/realms/Medelexis",
  "sub": "c64712b8-a25b-4368-82e0-a73d5fb7c944",
  "typ": "Bearer",
  "azp": "mis",
  "sid": "c6377cd2-2303-4984-b2b3-86585d1eaec7",
  "acr": "1",
  "allowed-origins": [
    "/*"
  ],
  "resource_access": {
    "mis": {
      "roles": [
        "user"
      ]
    }
  },
  "scope": "openid profile email",
  "email_verified": true,
  "name": "Test flawil",
  "preferred_username": "tester",
  "locale": "de",
  "given_name": "Test",
  "family_name": "flawil",
  "email": "[email protected]"
}

for all these combinations I receive

mis-echo-1  | OAuth2::AccessToken.from_hash: `hash` contained more than one 'token' key (["access_token", "id_token"]); using "access_token".
mis-echo-1  | E, [2024-09-05T14:47:40.218602 #1] ERROR -- : undefined method `[]' for nil:NilClass

col-panic avatar Sep 05 '24 14:09 col-panic

Fixed. (The error "contained more than one 'token' key" comes from an external library. I can't change it in my code.)

picman avatar Sep 06 '24 07:09 picman

thanks - will test on Monday!

col-panic avatar Sep 06 '24 08:09 col-panic

If I set resource_access.mis.roles then it won't let me login at all.

Both with and without the role set, I will see

mis-echo-1  | OAuth2::AccessToken.from_hash: `hash` contained more than one 'token' key (["access_token", "id_token"]); using "access_token".
mis-echo-1  | I, [2024-09-09T06:41:45.064722 #1]  INFO -- : Authentication failed due to a missing role in the token
mis-echo-1  | W, [2024-09-09T06:45:17.847620 #1]  WARN -- : Failed login for '[email protected]' from 80.108.2.151 at 2024-09-09 06:45:17 UTC
mis-echo-1  | E, [2024-09-09T06:45:17.847757 #1] ERROR -- : Benutzer oder Passwort ist ungültig.

But Keycloak tells me that the content of the access token should be

{
  "exp": 1725864532,
  "iat": 1725864232,
  "jti": "a165eff7-17f6-4f42-87bd-618cc093d013",
  "iss": "https://keycloak.medelexis.ch/realms/Medelexis",
  "sub": "c64712b8-a25b-4368-82e0-a73d5fb7c944",
  "typ": "Bearer",
  "azp": "mis",
  "sid": "1173cc58-9397-41b9-b9c5-c1e2406b5376",
  "acr": "1",
  "allowed-origins": [
    "/*"
  ],
  "resource_access": {
    "mis": {
      "roles": [
        "user"
      ]
    }
  },
  "scope": "openid profile email",
  "email_verified": true,
  "name": "Test flawil",
  "preferred_username": "tester",
  "locale": "de",
  "given_name": "Test",
  "family_name": "flawil",
  "email": "[email protected]"
}

Is there a means to show what access token redmine_oauth effectively gets? Maybe there's a problem in parsing the correct entry?

col-panic avatar Sep 09 '24 06:09 col-panic

I've added some debug log messages. Could you switch your log level to debug and post your output? (config.log_level = :debug in config/additional_environment.rb)

In my case I see:

  1. Validate user roles set to 'resource_access.mis.roles' nad roles present in the access_token:
DEBUG -- : Setting.validate_user_roles = 'resource_access.mis.roles'
DEBUG -- : key: resource_access
DEBUG -- : key: mis
DEBUG -- : key: roles
DEBUG -- : Roles: user
DEBUG -- : admin = false
DEBUG -- : try_to_log_in [email protected]
DEBUG -- : {:exp=>1725864532, :iat=>1725864232, :jti=>"a165eff7-17f6-4f42-87bd-618cc093d013", :iss=>"https://keycloak.medelexis.ch/realms/Medelexis", :sub=>"c64712b8-a25b-4368-82e0-a73d5fb7c944", :typ=>"Bearer", :azp=>"mis", :sid=>"1173cc58-9397-41b9-b9c5-c1e2406b5376", :acr=>"1", :"allowed-origins"=>["/*"], :resource_access=>{:mis=>{:roles=>["user"]}}, :scope=>"openid profile email", :email_verified=>true, :name=>"Test flawil", :preferred_username=>"tester", :locale=>"de", :given_name=>"Test", :family_name=>"flawil", :email=>"[email protected]"}
INFO -- : Successful authentication for 'kpicman'
  1. Roles are not present in the access_token:
DEBUG -- : Setting.validate_user_roles = 'resource_access.mis.roles'
DEBUG -- : key: resource_access
DEBUG -- : Key not found => access denied
DEBUG -- : Roles: 
DEBUG -- : user role not found => access denied
INFO -- : Authentication failed due to a missing role in the token
WARN -- : Failed login for '[email protected]' 

picman avatar Sep 11 '24 07:09 picman

I don't get it - on the left hand side you see the Evaluate Client Scope entry for access token, which clearly states that the resp. resource_access entry is part of it. Yet it seems not to pick it up?!

The interesting bit seems

mis-echo-1  | D, [2024-09-11T13:59:12.396448 #1] DEBUG -- : Setting.validate_user_roles = 'resource_access.mis.roles'
mis-echo-1  | D, [2024-09-11T13:59:12.396493 #1] DEBUG -- : key: resource_access
mis-echo-1  | D, [2024-09-11T13:59:12.396507 #1] DEBUG -- : Key not found => access denied

it's not even finding the base element?!

(Nevermind the No filters fit line .. thats a separate plugin. I removed it also to test, and it didn't change the behavior)

grafik

col-panic avatar Sep 11 '24 14:09 col-panic

Thats the client mapper setting for client roles grafik

col-panic avatar Sep 11 '24 14:09 col-panic

Maybe the problem is located in this line

mis-echo-1  | OAuth2::AccessToken.from_hash: `hash` contained more than one 'token' key (["access_token", "id_token"]); using "access_token".

the info I transport is either part of access_token or id_token but these are encoded as JWT. I'm not sure you are analyzing this. Could you output the response you get at this point?

col-panic avatar Sep 11 '24 14:09 col-panic

After studying documentation and source codes I came to a conclusion that the roles information are a part of id_token not access_token. Despite your option Add to access token. Problem is that if both tokens are present in the response, the OAuth2 library takes the first one. In your case it is access_token that doesn't contain roles. From oauth2 gem:

TOKEN_KEYS_STR = %w[access_token id_token token accessToken idToken].freeze
TOKEN_KEYS_SYM = %i[access_token id_token token accessToken idToken].freeze
 TOKEN_KEY_LOOKUP = TOKEN_KEYS_STR + TOKEN_KEYS_SYM

supported_keys = TOKEN_KEY_LOOKUP & fresh.keys
key = supported_keys[0]

So they simply takes the first available token and there is no option to change it. Can't you somehow set in your configuration to the response contains id_token only and not access_token?

picman avatar Sep 12 '24 07:09 picman

After checking your calls, I don't seem there is a way for me to intervene in the response.

Lets fix the calls that happen (from my live debugging)

  1. Redirected to https://keycloak.medelexis.ch/realms/Medelexis/protocol/openid-connect/auth?client_id=mis&redirect_uri=https%3A%2F%2Fmis-echo.medelexis.ch%2Foauth2callback&response_type=code&scope=openid+email&state=92hNDQ7yPNGPt%2F%2FD%2BLazs5xZx6g6tUn2Tbx66TJrlfM%3D
  2. Started GET "/oauth2callback?state=92hNDQ7yPNGPt%2F%2FD%2BLazs5xZx6g6tUn2Tbx66TJrlfM%3D&session_state=c72ab908-32b4-41a0-a774-f1145e0af2fd&iss=https%3A%2F%2Fkeycloak.medelexis.ch%2Frealms%2FMedelexis&code=2a630c94-47e8-44c0-b7fa-52bba6372e81.c72ab908-32b4-41a0-a774-f1145e0af2fd.b7d66e22-37d0-4915-b3cc-13b21c65f233" for 80.108.2.151 at 2024-09-12 07:50:13 +0000
  1. Is an "Authentication Request" (see https://www.amazon.com/Keycloak-Management-Applications-protocols-applications/dp/1800562497 page 44) which creates an authorization code which is returned to the application in 2
  2. The code entry is the authorization code which the application uses to obtain the ID token and the refresh token.
  3. This code you would use to connect to the token endpoint to get the actual token (I'm confused, because I don't see this request to https://keycloak.medelexis.ch/realms/Medelexis/protocol/openid-connect/token happen in the redmine log) which would look like 4)
  4. see book right bottom Groß (IMG_8250)

This token response is not meant to contain the respective info, but the id_token is (afaik the access token is only in keycloak a JWT, it is not required to be of any known kind to other oauth2 idps) - hence the content of it should be analyzed.

But I guess another approach would be to use the authorization code access token to query the userinfo endpoint.

col-panic avatar Sep 12 '24 08:09 col-panic

I've added a new option into the plugin's settings. You can now chose a preferable token type. Please give it a try.

picman avatar Sep 13 '24 09:09 picman

removed (content was clearly wrong)

col-panic avatar Sep 13 '24 10:09 col-panic

I added the following line Rails.logger.debug { "Decoded token: #{user_info.to_json}" } after https://github.com/kontron/redmine_oauth/blob/bc7e4e40c85680943b8761dae95f50cbd74f1ab7/app/controllers/redmine_oauth_controller.rb#L103 which now leads to the following ouput

-echo-1  | I, [2024-09-13T10:54:43.718583 #1]  INFO -- :   Current user: anonymous
mis-echo-1  | OAuth2::AccessToken.from_hash: `hash` contained more than one 'token' key (["access_token", "id_token"]); using "id_token".
mis-echo-1  | D, [2024-09-13T10:54:43.763172 #1] DEBUG -- : Decoded token: {"exp":1726225183,"iat":1726224883,"auth_time":1726223264,"jti":"6ee9e5f5-fca1-4b2e-a882-7bd83102c1c7","iss":"https://keycloak.medelexis.ch/realms/Medelexis","aud":"mis","sub":"c64712b8-a25b-4368-82e0-a73d5fb7c944","typ":"ID","azp":"mis","sid":"81765aa5-87b8-4666-8c55-cf7f8cdf657e","at_hash":"piggBPy814R-XJvOE36EIg","acr":"0","email_verified":true,"name":"Test flawil","preferred_username":"tester","user_roles":["user"],"locale":"de","given_name":"Test","family_name":"flawil","email":"[email protected]"}
mis-echo-1  | D, [2024-09-13T10:54:43.763218 #1] DEBUG -- : Setting.validate_user_roles = '"user_roles"'
mis-echo-1  | D, [2024-09-13T10:54:43.763241 #1] DEBUG -- : key: "user_roles"
mis-echo-1  | D, [2024-09-13T10:54:43.763285 #1] DEBUG -- : Key not found => access denied
mis-echo-1  | D, [2024-09-13T10:54:43.763312 #1] DEBUG -- : Roles: 
mis-echo-1  | D, [2024-09-13T10:54:43.763325 #1] DEBUG -- : user role not found => access denied
mis-echo-1  | I, [2024-09-13T10:54:43.763338 #1]  INFO -- : Authentication failed due to a missing role in the token
mis-echo-1  | W, [2024-09-13T10:54:43.763392 #1]  WARN -- : Failed login for '[email protected]' from 80.108.2.151 at 2024-09-13 10:54:43 UTC

For easier analysis i formatted the decoded token:

{
    "exp": 1726225183,
    "iat": 1726224883,
    "auth_time": 1726223264,
    "jti": "6ee9e5f5-fca1-4b2e-a882-7bd83102c1c7",
    "iss": "https://keycloak.medelexis.ch/realms/Medelexis",
    "aud": "mis",
    "sub": "c64712b8-a25b-4368-82e0-a73d5fb7c944",
    "typ": "ID",
    "azp": "mis",
    "sid": "81765aa5-87b8-4666-8c55-cf7f8cdf657e",
    "at_hash": "piggBPy814R-XJvOE36EIg",
    "acr": "0",
    "email_verified": true,
    "name": "Test flawil",
    "preferred_username": "tester",
    "user_roles": [
        "user"
    ],
    "locale": "de",
    "given_name": "Test",
    "family_name": "flawil",
    "email": "[email protected]"
}

clearly the value is in there - what is wrong here??

col-panic avatar Sep 13 '24 10:09 col-panic

Here seems to be a problem:

my environment: DEBUG -- : Setting.validate_user_roles = 'user_roles' your environment: DEBUG -- : Setting.validate_user_roles = '"user_roles"'

Notice the double quotation marks around user_roles. Do you happen to enter user_roles with double quotation marks in your plugin's settings?

picman avatar Sep 13 '24 11:09 picman

you are right, I removed the quotation marks, yet the problem persists:

is-echo-1  | I, [2024-09-13T11:52:36.748956 #1]  INFO -- :   Current user: anonymous
mis-echo-1  | OAuth2::AccessToken.from_hash: `hash` contained more than one 'token' key (["access_token", "id_token"]); using "access_token".
mis-echo-1  | D, [2024-09-13T11:52:36.799044 #1] DEBUG -- : Setting.validate_user_roles = 'user_roles'
mis-echo-1  | D, [2024-09-13T11:52:36.799201 #1] DEBUG -- : key: user_roles
mis-echo-1  | D, [2024-09-13T11:52:36.799428 #1] DEBUG -- : Key not found => access denied
mis-echo-1  | D, [2024-09-13T11:52:36.799514 #1] DEBUG -- : Roles: 
mis-echo-1  | D, [2024-09-13T11:52:36.799730 #1] DEBUG -- : user role not found => access denied
mis-echo-1  | I, [2024-09-13T11:52:36.799774 #1]  INFO -- : Authentication failed due to a missing role in the token

col-panic avatar Sep 13 '24 11:09 col-panic

I've added a debug message with user_info content. git pull roles branch and post your log again, please.

picman avatar Sep 13 '24 11:09 picman

mis-echo-1  | OAuth2::AccessToken.from_hash: `hash` contained more than one 'token' key (["access_token", "id_token"]); using "id_token".
mis-echo-1  | D, [2024-09-13T12:03:15.060896 #1] DEBUG -- : Setting.validate_user_roles = 'user_roles.'
mis-echo-1  | D, [2024-09-13T12:03:15.060943 #1] DEBUG -- : {"exp"=>1726229295, "iat"=>1726228995, "auth_time"=>1726227416, "jti"=>"b62a791b-5c6c-47cc-880b-7b2fa7e4204d", "iss"=>"https://keycloak.medelexis.ch/realms/Medelexis", "aud"=>"mis", "sub"=>"c64712b8-a25b-4368-82e0-a73d5fb7c944", "typ"=>"ID", "azp"=>"mis", "sid"=>"1f8783d5-a7db-4861-b37f-f65f72bc9bad", "at_hash"=>"vpCP80GKDS7qrmO95Rgr-g", "acr"=>"0", "email_verified"=>true, "name"=>"Test flawil", "preferred_username"=>"tester", "user_roles"=>["user"], "locale"=>"de", "given_name"=>"Test", "family_name"=>"flawil", "email"=>"[email protected]", "login"=>"tester"}
mis-echo-1  | D, [2024-09-13T12:03:15.061015 #1] DEBUG -- : key: user_roles
mis-echo-1  | D, [2024-09-13T12:03:15.061047 #1] DEBUG -- : Key not found => access denied
mis-echo-1  | D, [2024-09-13T12:03:15.061064 #1] DEBUG -- : Roles: 
mis-echo-1  | D, [2024-09-13T12:03:15.061076 #1] DEBUG -- : user role not found => access denied
mis-echo-1  | I, [2024-09-13T12:03:15.061103 #1]  INFO -- : Authentication failed due to a missing role in the token

col-panic avatar Sep 13 '24 12:09 col-panic

Once again, please.

picman avatar Sep 13 '24 12:09 picman

This looks very good now @picman

It worked with the id_token and the key user_roles - but as this was an adaptation, i fixed the defaults of keycloak. So it should directly work with keycloak if:

The keycloak instance creates a client and adds the following client roles:

grafik

Now the user role has to be set for a user. But the roles are only transported by default with the access_token, if the scope roles is either requested by the client (which is NOT the case for redmine_oauth at the moment) or set to Default in keycloak (in which case the info will be transmitted without requesting the scope)

grafik

The keycloak scope roles will then automatically add the info to the access_token (!NOT! the id_token) by the key resource_access.${client_id}.roles which can be analysed as follows (my client is called mis)

grafik

I also tested dynamically assigning the administrator - and it worked as expected!

Thank you very much! I'm not sure about the requirement of selecting the token you patched in https://github.com/kontron/redmine_oauth/commit/fddd607753f1b8d4f05bba78f958d27423e5c735 - the default behaviour for keycloak should be to select the access_tokenas described!

col-panic avatar Sep 13 '24 12:09 col-panic

All right. I reverted the patch. Can you test it with roles_without_patch branch, please?

picman avatar Sep 13 '24 12:09 picman

The branch roles_without_patch works like a charm, and the log info OAuth2::AccessToken.from_hash: hash contained more than one 'token' key (["access_token", "id_token"]); using "access_token". is just correct for the Keycloak setting.

Thank you very much for your work - maybe we should somehow link the info in this issue to the documentation! 👍

col-panic avatar Sep 13 '24 12:09 col-panic

Merged into devel.

picman avatar Sep 13 '24 14:09 picman