diff --git a/docs/configuration/oidc.md b/docs/configuration/oidc.md index c5ac4a6e..9018b588 100644 --- a/docs/configuration/oidc.md +++ b/docs/configuration/oidc.md @@ -60,7 +60,7 @@ oidc-client-secret: "" # Array of string. Scopes to request from the OIDC provider. The returned values will be used to # populate users created in GtS as a result of the authentication flow. 'openid' and 'email' are required. # 'profile' is used to extract a username for the newly created user. -# 'groups' is optional and can be used to determine if a user is an admin (if they're in the group 'admin' or 'admins'). +# 'groups' is optional and can be used to determine if a user is an admin based on oidc-admin-groups. # Examples: See eg., https://auth0.com/docs/scopes/openid-connect-scopes # Default: ["openid", "email", "profile", "groups"] oidc-scopes: @@ -75,6 +75,12 @@ oidc-scopes: # Options: [true, false] # Default: false oidc-link-existing: false + +# Array of string. If the returned ID token contains a 'groups' claim that +# matches one of the groups in oidc-admin-groups, then this user will be granted +# admin rights on the GtS instance +# Default: [] +oidc-admin-groups: [] ``` ## Behavior @@ -101,7 +107,7 @@ access to your GtS account. Most OIDC providers allow for the concept of groups and group memberships in returned claims. GoToSocial can use group membership to determine whether or not a user returned from an OIDC flow should be created as an admin account or not. -If the returned OIDC groups information for a user contains membership of the groups `admin` or `admins`, then that user will be created/signed in as though they are an admin. +If the returned OIDC groups information for a user contains membership of the groups configured in `oidc-admin-groups`, then that user will be created/signed in as though they are an admin. ## Migrating from old versions diff --git a/example/config.yaml b/example/config.yaml index 2fab24ab..2239e0ba 100644 --- a/example/config.yaml +++ b/example/config.yaml @@ -616,7 +616,7 @@ oidc-client-secret: "" # Array of string. Scopes to request from the OIDC provider. The returned values will be used to # populate users created in GtS as a result of the authentication flow. 'openid' and 'email' are required. # 'profile' is used to extract a username for the newly created user. -# 'groups' is optional and can be used to determine if a user is an admin (if they're in the group 'admin' or 'admins'). +# 'groups' is optional and can be used to determine if a user is an admin based on oidc-admin-groups. # Examples: See eg., https://auth0.com/docs/scopes/openid-connect-scopes # Default: ["openid", "email", "profile", "groups"] oidc-scopes: @@ -632,6 +632,11 @@ oidc-scopes: # Default: false oidc-link-existing: false +# Array of string. If the returned ID token contains a 'groups' claim that matches one of the +# groups in oidc-admin-groups, then this user will be granted admin rights on the GtS instance +# Default: [] +oidc-admin-groups: [] + ####################### ##### SMTP CONFIG ##### ####################### diff --git a/internal/api/auth/callback.go b/internal/api/auth/callback.go index bef7b013..5f0425de 100644 --- a/internal/api/auth/callback.go +++ b/internal/api/auth/callback.go @@ -284,10 +284,15 @@ func (m *Module) createUserFromOIDC(ctx context.Context, claims *oidc.Claims, ex } // check if the user is in any recognised admin groups + adminGroups := config.GetOIDCAdminGroups() var admin bool +LOOP: for _, g := range claims.Groups { - if strings.EqualFold(g, "admin") || strings.EqualFold(g, "admins") { - admin = true + for _, ag := range adminGroups { + if strings.EqualFold(g, ag) { + admin = true + break LOOP + } } } diff --git a/internal/config/config.go b/internal/config/config.go index 1dea9078..5673b76d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -122,6 +122,7 @@ type Configuration struct { OIDCClientSecret string `name:"oidc-client-secret" usage:"ClientSecret of GoToSocial, as registered with the OIDC provider."` OIDCScopes []string `name:"oidc-scopes" usage:"OIDC scopes."` OIDCLinkExisting bool `name:"oidc-link-existing" usage:"link existing user accounts to OIDC logins based on the stored email value"` + OIDCAdminGroups []string `name:"oidc-admin-groups" usage:"Membership of one of the listed groups makes someone a GtS admin"` SMTPHost string `name:"smtp-host" usage:"Host of the smtp server. Eg., 'smtp.eu.mailgun.org'"` SMTPPort int `name:"smtp-port" usage:"Port of the smtp server. Eg., 587"` diff --git a/internal/config/helpers.gen.go b/internal/config/helpers.gen.go index 1a4c14a8..41c56a57 100644 --- a/internal/config/helpers.gen.go +++ b/internal/config/helpers.gen.go @@ -1724,6 +1724,31 @@ func GetOIDCLinkExisting() bool { return global.GetOIDCLinkExisting() } // SetOIDCLinkExisting safely sets the value for global configuration 'OIDCLinkExisting' field func SetOIDCLinkExisting(v bool) { global.SetOIDCLinkExisting(v) } +// GetOIDCAdminGroups safely fetches the Configuration value for state's 'OIDCAdminGroups' field +func (st *ConfigState) GetOIDCAdminGroups() (v []string) { + st.mutex.Lock() + v = st.config.OIDCAdminGroups + st.mutex.Unlock() + return +} + +// SetOIDCAdminGroups safely sets the Configuration value for state's 'OIDCAdminGroups' field +func (st *ConfigState) SetOIDCAdminGroups(v []string) { + st.mutex.Lock() + defer st.mutex.Unlock() + st.config.OIDCAdminGroups = v + st.reloadToViper() +} + +// OIDCAdminGroupsFlag returns the flag name for the 'OIDCAdminGroups' field +func OIDCAdminGroupsFlag() string { return "oidc-admin-groups" } + +// GetOIDCAdminGroups safely fetches the value for global configuration 'OIDCAdminGroups' field +func GetOIDCAdminGroups() []string { return global.GetOIDCAdminGroups() } + +// SetOIDCAdminGroups safely sets the value for global configuration 'OIDCAdminGroups' field +func SetOIDCAdminGroups(v []string) { global.SetOIDCAdminGroups(v) } + // GetSMTPHost safely fetches the Configuration value for state's 'SMTPHost' field func (st *ConfigState) GetSMTPHost() (v string) { st.mutex.Lock() diff --git a/test/envparsing.sh b/test/envparsing.sh index 2c725f23..10c5cb3a 100755 --- a/test/envparsing.sh +++ b/test/envparsing.sh @@ -2,7 +2,7 @@ set -eu -EXPECT='{"account-domain":"peepee","accounts-allow-custom-css":true,"accounts-approval-required":false,"accounts-reason-required":false,"accounts-registration-open":true,"advanced-cookies-samesite":"strict","advanced-rate-limit-requests":6969,"advanced-throttling-multiplier":-1,"advanced-throttling-retry-after":10000000000,"application-name":"gts","bind-address":"127.0.0.1","cache":{"gts":{"account-max-size":99,"account-sweep-freq":1000000000,"account-ttl":10800000000000,"block-max-size":100,"block-sweep-freq":10000000000,"block-ttl":300000000000,"domain-block-max-size":1000,"domain-block-sweep-freq":60000000000,"domain-block-ttl":86400000000000,"emoji-category-max-size":100,"emoji-category-sweep-freq":10000000000,"emoji-category-ttl":300000000000,"emoji-max-size":500,"emoji-sweep-freq":10000000000,"emoji-ttl":300000000000,"mention-max-size":500,"mention-sweep-freq":10000000000,"mention-ttl":300000000000,"notification-max-size":500,"notification-sweep-freq":10000000000,"notification-ttl":300000000000,"report-max-size":100,"report-sweep-freq":10000000000,"report-ttl":300000000000,"status-max-size":500,"status-sweep-freq":10000000000,"status-ttl":300000000000,"tombstone-max-size":100,"tombstone-sweep-freq":10000000000,"tombstone-ttl":300000000000,"user-max-size":100,"user-sweep-freq":10000000000,"user-ttl":300000000000}},"config-path":"internal/config/testdata/test.yaml","db-address":":memory:","db-database":"gotosocial_prod","db-max-open-conns-multiplier":3,"db-password":"hunter2","db-port":6969,"db-sqlite-busy-timeout":1000000000,"db-sqlite-cache-size":0,"db-sqlite-journal-mode":"DELETE","db-sqlite-synchronous":"FULL","db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"sqlite","db-user":"sex-haver","dry-run":true,"email":"","host":"example.com","instance-deliver-to-shared-inboxes":false,"instance-expose-peers":true,"instance-expose-public-timeline":true,"instance-expose-suspended":true,"instance-expose-suspended-web":true,"landing-page-user":"admin","letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":true,"letsencrypt-port":80,"log-db-queries":true,"log-level":"info","media-description-max-chars":5000,"media-description-min-chars":69,"media-emoji-local-max-size":420,"media-emoji-remote-max-size":420,"media-image-max-size":420,"media-remote-cache-days":30,"media-video-max-size":420,"oidc-client-id":"1234","oidc-client-secret":"shhhh its a secret","oidc-enabled":true,"oidc-idp-name":"sex-haver","oidc-issuer":"whoknows","oidc-link-existing":true,"oidc-scopes":["read","write"],"oidc-skip-verification":true,"password":"","path":"","port":6969,"protocol":"http","request-id-header":"X-Trace-Id","smtp-from":"queen.rip.in.piss@terfisland.org","smtp-host":"example.com","smtp-password":"hunter2","smtp-port":4269,"smtp-username":"sex-haver","software-version":"","statuses-cw-max-chars":420,"statuses-max-chars":69,"statuses-media-max-files":1,"statuses-poll-max-options":1,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/root/store","storage-s3-access-key":"minio","storage-s3-bucket":"gts","storage-s3-endpoint":"localhost:9000","storage-s3-proxy":true,"storage-s3-secret-key":"miniostorage","storage-s3-use-ssl":false,"syslog-address":"127.0.0.1:6969","syslog-enabled":true,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32","docker.host.local"],"username":"","web-asset-base-dir":"/root","web-template-base-dir":"/root"}' +EXPECT='{"account-domain":"peepee","accounts-allow-custom-css":true,"accounts-approval-required":false,"accounts-reason-required":false,"accounts-registration-open":true,"advanced-cookies-samesite":"strict","advanced-rate-limit-requests":6969,"advanced-throttling-multiplier":-1,"advanced-throttling-retry-after":10000000000,"application-name":"gts","bind-address":"127.0.0.1","cache":{"gts":{"account-max-size":99,"account-sweep-freq":1000000000,"account-ttl":10800000000000,"block-max-size":100,"block-sweep-freq":10000000000,"block-ttl":300000000000,"domain-block-max-size":1000,"domain-block-sweep-freq":60000000000,"domain-block-ttl":86400000000000,"emoji-category-max-size":100,"emoji-category-sweep-freq":10000000000,"emoji-category-ttl":300000000000,"emoji-max-size":500,"emoji-sweep-freq":10000000000,"emoji-ttl":300000000000,"mention-max-size":500,"mention-sweep-freq":10000000000,"mention-ttl":300000000000,"notification-max-size":500,"notification-sweep-freq":10000000000,"notification-ttl":300000000000,"report-max-size":100,"report-sweep-freq":10000000000,"report-ttl":300000000000,"status-max-size":500,"status-sweep-freq":10000000000,"status-ttl":300000000000,"tombstone-max-size":100,"tombstone-sweep-freq":10000000000,"tombstone-ttl":300000000000,"user-max-size":100,"user-sweep-freq":10000000000,"user-ttl":300000000000}},"config-path":"internal/config/testdata/test.yaml","db-address":":memory:","db-database":"gotosocial_prod","db-max-open-conns-multiplier":3,"db-password":"hunter2","db-port":6969,"db-sqlite-busy-timeout":1000000000,"db-sqlite-cache-size":0,"db-sqlite-journal-mode":"DELETE","db-sqlite-synchronous":"FULL","db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"sqlite","db-user":"sex-haver","dry-run":true,"email":"","host":"example.com","instance-deliver-to-shared-inboxes":false,"instance-expose-peers":true,"instance-expose-public-timeline":true,"instance-expose-suspended":true,"instance-expose-suspended-web":true,"landing-page-user":"admin","letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":true,"letsencrypt-port":80,"log-db-queries":true,"log-level":"info","media-description-max-chars":5000,"media-description-min-chars":69,"media-emoji-local-max-size":420,"media-emoji-remote-max-size":420,"media-image-max-size":420,"media-remote-cache-days":30,"media-video-max-size":420,"oidc-admin-groups":["steamy"],"oidc-client-id":"1234","oidc-client-secret":"shhhh its a secret","oidc-enabled":true,"oidc-idp-name":"sex-haver","oidc-issuer":"whoknows","oidc-link-existing":true,"oidc-scopes":["read","write"],"oidc-skip-verification":true,"password":"","path":"","port":6969,"protocol":"http","request-id-header":"X-Trace-Id","smtp-from":"queen.rip.in.piss@terfisland.org","smtp-host":"example.com","smtp-password":"hunter2","smtp-port":4269,"smtp-username":"sex-haver","software-version":"","statuses-cw-max-chars":420,"statuses-max-chars":69,"statuses-media-max-files":1,"statuses-poll-max-options":1,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/root/store","storage-s3-access-key":"minio","storage-s3-bucket":"gts","storage-s3-endpoint":"localhost:9000","storage-s3-proxy":true,"storage-s3-secret-key":"miniostorage","storage-s3-use-ssl":false,"syslog-address":"127.0.0.1:6969","syslog-enabled":true,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32","docker.host.local"],"username":"","web-asset-base-dir":"/root","web-template-base-dir":"/root"}' # Set all the environment variables to # ensure that these are parsed without panic @@ -72,6 +72,7 @@ GTS_OIDC_CLIENT_ID='1234' \ GTS_OIDC_CLIENT_SECRET='shhhh its a secret' \ GTS_OIDC_SCOPES='read,write' \ GTS_OIDC_LINK_EXISTING=true \ +GTS_OIDC_ADMIN_GROUPS='steamy' \ GTS_SMTP_HOST='example.com' \ GTS_SMTP_PORT=4269 \ GTS_SMTP_USERNAME='sex-haver' \