mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-11-01 06:50:00 +00:00
[feature/oidc] Add support for very basic RBAC (#2642)
* Add support for very basic RBAC * Add some small tests for allowedGroup and adminGroup * Switch to table-driven tests
This commit is contained in:
parent
feb6abbab2
commit
9bf448be7a
8 changed files with 130 additions and 7 deletions
|
@ -79,6 +79,12 @@ oidc-scopes:
|
||||||
# Default: false
|
# Default: false
|
||||||
oidc-link-existing: 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-allowed-groups, then this user will be granted access on the GtS instance. If the array is empty,
|
||||||
|
# then all groups will be granted permission.
|
||||||
|
# Default: []
|
||||||
|
oidc-allowed-groups: []
|
||||||
|
|
||||||
# Array of string. If the returned ID token contains a 'groups' claim that matches one of the
|
# 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
|
# groups in oidc-admin-groups, then this user will be granted admin rights on the GtS instance
|
||||||
# Default: []
|
# Default: []
|
||||||
|
|
|
@ -729,6 +729,12 @@ oidc-scopes:
|
||||||
# Default: false
|
# Default: false
|
||||||
oidc-link-existing: 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-allowed-groups, then this user will be granted access on the GtS instance. If the array is empty,
|
||||||
|
# then all groups will be granted permission.
|
||||||
|
# Default: []
|
||||||
|
oidc-allowed-groups: []
|
||||||
|
|
||||||
# Array of string. If the returned ID token contains a 'groups' claim that matches one of the
|
# 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
|
# groups in oidc-admin-groups, then this user will be granted admin rights on the GtS instance
|
||||||
# Default: []
|
# Default: []
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-contrib/sessions"
|
"github.com/gin-contrib/sessions"
|
||||||
|
@ -156,6 +157,14 @@ func (m *Module) CallbackGETHandler(c *gin.Context) {
|
||||||
apiutil.TemplateWebPage(c, page)
|
apiutil.TemplateWebPage(c, page)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check user permissions on login
|
||||||
|
if !allowedGroup(claims.Groups) {
|
||||||
|
err := fmt.Errorf("User groups %+v do not include an allowed group", claims.Groups)
|
||||||
|
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
s.Set(sessionUserID, user.ID)
|
s.Set(sessionUserID, user.ID)
|
||||||
if err := s.Save(); err != nil {
|
if err := s.Save(); err != nil {
|
||||||
m.clearSession(s)
|
m.clearSession(s)
|
||||||
|
@ -297,6 +306,11 @@ func (m *Module) createUserFromOIDC(ctx context.Context, claims *oidc.Claims, ex
|
||||||
return nil, gtserror.NewErrorConflict(err, help)
|
return nil, gtserror.NewErrorConflict(err, help)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !allowedGroup(claims.Groups) {
|
||||||
|
err := fmt.Errorf("User groups %+v do not include an allowed group", claims.Groups)
|
||||||
|
return nil, gtserror.NewErrorUnauthorized(err, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
// We still need to set something as a password, even
|
// We still need to set something as a password, even
|
||||||
// if it's not a password the user will end up using.
|
// if it's not a password the user will end up using.
|
||||||
//
|
//
|
||||||
|
@ -356,17 +370,37 @@ func (m *Module) createUserFromOIDC(ctx context.Context, claims *oidc.Claims, ex
|
||||||
// adminGroup returns true if one of the given OIDC
|
// adminGroup returns true if one of the given OIDC
|
||||||
// groups is equal to at least one admin OIDC group.
|
// groups is equal to at least one admin OIDC group.
|
||||||
func adminGroup(groups []string) bool {
|
func adminGroup(groups []string) bool {
|
||||||
for _, ag := range config.GetOIDCAdminGroups() {
|
adminGroups := config.GetOIDCAdminGroups()
|
||||||
for _, g := range groups {
|
for _, claimedGroup := range groups {
|
||||||
if strings.EqualFold(ag, g) {
|
if slices.ContainsFunc(adminGroups, func(allowedGroup string) bool {
|
||||||
// This is an admin group,
|
return strings.EqualFold(claimedGroup, allowedGroup)
|
||||||
// ∴ user is an admin.
|
}) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// User is in no admin groups,
|
// User is in no admin groups,
|
||||||
// ∴ user is not an admin.
|
// ∴ user is not an admin.
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// allowedGroup returns true if one of the given OIDC
|
||||||
|
// groups is equal to at least one allowed OIDC group.
|
||||||
|
func allowedGroup(groups []string) bool {
|
||||||
|
allowedGroups := config.GetOIDCAllowedGroups()
|
||||||
|
if len(allowedGroups) == 0 {
|
||||||
|
// If no groups are configured, allow access (for backwards compatibility)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, claimedGroup := range groups {
|
||||||
|
if slices.ContainsFunc(allowedGroups, func(allowedGroup string) bool {
|
||||||
|
return strings.EqualFold(claimedGroup, allowedGroup)
|
||||||
|
}) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// User is in no allowed groups,
|
||||||
|
// ∴ user is not allowed to log in
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
45
internal/api/auth/callback_test.go
Normal file
45
internal/api/auth/callback_test.go
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAdminGroup(t *testing.T) {
|
||||||
|
testrig.InitTestConfig()
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
groups []string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{name: "not in admin group", groups: []string{"group1", "group2", "allowedRole"}, expected: false},
|
||||||
|
{name: "in admin group", groups: []string{"group1", "group2", "adminRole"}, expected: true},
|
||||||
|
} {
|
||||||
|
test := test // loopvar capture
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
if got := adminGroup(test.groups); got != test.expected {
|
||||||
|
t.Fatalf("got: %t, wanted: %t", got, test.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAllowedGroup(t *testing.T) {
|
||||||
|
testrig.InitTestConfig()
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
groups []string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{name: "not in allowed group", groups: []string{"group1", "group2", "adminRole"}, expected: false},
|
||||||
|
{name: "in allowed group", groups: []string{"group1", "group2", "allowedRole"}, expected: true},
|
||||||
|
} {
|
||||||
|
test := test // loopvar capture
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
if got := allowedGroup(test.groups); got != test.expected {
|
||||||
|
t.Fatalf("got: %t, wanted: %t", got, test.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -133,6 +133,7 @@ type Configuration struct {
|
||||||
OIDCClientSecret string `name:"oidc-client-secret" usage:"ClientSecret of GoToSocial, as registered with the OIDC provider."`
|
OIDCClientSecret string `name:"oidc-client-secret" usage:"ClientSecret of GoToSocial, as registered with the OIDC provider."`
|
||||||
OIDCScopes []string `name:"oidc-scopes" usage:"OIDC scopes."`
|
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"`
|
OIDCLinkExisting bool `name:"oidc-link-existing" usage:"link existing user accounts to OIDC logins based on the stored email value"`
|
||||||
|
OIDCAllowedGroups []string `name:"oidc-allowed-groups" usage:"Membership of one of the listed groups allows access to GtS. If this is empty, all groups are allowed."`
|
||||||
OIDCAdminGroups []string `name:"oidc-admin-groups" usage:"Membership of one of the listed groups makes someone a GtS admin"`
|
OIDCAdminGroups []string `name:"oidc-admin-groups" usage:"Membership of one of the listed groups makes someone a GtS admin"`
|
||||||
|
|
||||||
TracingEnabled bool `name:"tracing-enabled" usage:"Enable OTLP Tracing"`
|
TracingEnabled bool `name:"tracing-enabled" usage:"Enable OTLP Tracing"`
|
||||||
|
|
|
@ -1975,6 +1975,31 @@ func GetOIDCLinkExisting() bool { return global.GetOIDCLinkExisting() }
|
||||||
// SetOIDCLinkExisting safely sets the value for global configuration 'OIDCLinkExisting' field
|
// SetOIDCLinkExisting safely sets the value for global configuration 'OIDCLinkExisting' field
|
||||||
func SetOIDCLinkExisting(v bool) { global.SetOIDCLinkExisting(v) }
|
func SetOIDCLinkExisting(v bool) { global.SetOIDCLinkExisting(v) }
|
||||||
|
|
||||||
|
// GetOIDCAllowedGroups safely fetches the Configuration value for state's 'OIDCAllowedGroups' field
|
||||||
|
func (st *ConfigState) GetOIDCAllowedGroups() (v []string) {
|
||||||
|
st.mutex.RLock()
|
||||||
|
v = st.config.OIDCAllowedGroups
|
||||||
|
st.mutex.RUnlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetOIDCAllowedGroups safely sets the Configuration value for state's 'OIDCAllowedGroups' field
|
||||||
|
func (st *ConfigState) SetOIDCAllowedGroups(v []string) {
|
||||||
|
st.mutex.Lock()
|
||||||
|
defer st.mutex.Unlock()
|
||||||
|
st.config.OIDCAllowedGroups = v
|
||||||
|
st.reloadToViper()
|
||||||
|
}
|
||||||
|
|
||||||
|
// OIDCAllowedGroupsFlag returns the flag name for the 'OIDCAllowedGroups' field
|
||||||
|
func OIDCAllowedGroupsFlag() string { return "oidc-allowed-groups" }
|
||||||
|
|
||||||
|
// GetOIDCAllowedGroups safely fetches the value for global configuration 'OIDCAllowedGroups' field
|
||||||
|
func GetOIDCAllowedGroups() []string { return global.GetOIDCAllowedGroups() }
|
||||||
|
|
||||||
|
// SetOIDCAllowedGroups safely sets the value for global configuration 'OIDCAllowedGroups' field
|
||||||
|
func SetOIDCAllowedGroups(v []string) { global.SetOIDCAllowedGroups(v) }
|
||||||
|
|
||||||
// GetOIDCAdminGroups safely fetches the Configuration value for state's 'OIDCAdminGroups' field
|
// GetOIDCAdminGroups safely fetches the Configuration value for state's 'OIDCAdminGroups' field
|
||||||
func (st *ConfigState) GetOIDCAdminGroups() (v []string) {
|
func (st *ConfigState) GetOIDCAdminGroups() (v []string) {
|
||||||
st.mutex.RLock()
|
st.mutex.RLock()
|
||||||
|
|
|
@ -119,6 +119,9 @@ EXPECT=$(cat << "EOF"
|
||||||
"oidc-admin-groups": [
|
"oidc-admin-groups": [
|
||||||
"steamy"
|
"steamy"
|
||||||
],
|
],
|
||||||
|
"oidc-allowed-groups": [
|
||||||
|
"sloths"
|
||||||
|
],
|
||||||
"oidc-client-id": "1234",
|
"oidc-client-id": "1234",
|
||||||
"oidc-client-secret": "shhhh its a secret",
|
"oidc-client-secret": "shhhh its a secret",
|
||||||
"oidc-enabled": true,
|
"oidc-enabled": true,
|
||||||
|
@ -252,6 +255,7 @@ GTS_OIDC_CLIENT_ID='1234' \
|
||||||
GTS_OIDC_CLIENT_SECRET='shhhh its a secret' \
|
GTS_OIDC_CLIENT_SECRET='shhhh its a secret' \
|
||||||
GTS_OIDC_SCOPES='read,write' \
|
GTS_OIDC_SCOPES='read,write' \
|
||||||
GTS_OIDC_LINK_EXISTING=true \
|
GTS_OIDC_LINK_EXISTING=true \
|
||||||
|
GTS_OIDC_ALLOWED_GROUPS='sloths' \
|
||||||
GTS_OIDC_ADMIN_GROUPS='steamy' \
|
GTS_OIDC_ADMIN_GROUPS='steamy' \
|
||||||
GTS_SMTP_HOST='example.com' \
|
GTS_SMTP_HOST='example.com' \
|
||||||
GTS_SMTP_PORT=4269 \
|
GTS_SMTP_PORT=4269 \
|
||||||
|
|
|
@ -119,6 +119,8 @@ func InitTestConfig() {
|
||||||
OIDCClientSecret: "",
|
OIDCClientSecret: "",
|
||||||
OIDCScopes: []string{oidc.ScopeOpenID, "profile", "email", "groups"},
|
OIDCScopes: []string{oidc.ScopeOpenID, "profile", "email", "groups"},
|
||||||
OIDCLinkExisting: false,
|
OIDCLinkExisting: false,
|
||||||
|
OIDCAdminGroups: []string{"adminRole"},
|
||||||
|
OIDCAllowedGroups: []string{"allowedRole"},
|
||||||
|
|
||||||
SMTPHost: "",
|
SMTPHost: "",
|
||||||
SMTPPort: 0,
|
SMTPPort: 0,
|
||||||
|
|
Loading…
Reference in a new issue