feat: synchronize oidc user roles (#8595)

* feat: oidc user role sync
User roles come from oidc claims. Prevent manual user role changes
if set.
* allow mapping 1:many
This commit is contained in:
Steven Masley
2023-07-24 08:34:24 -04:00
committed by GitHub
parent 94541d201f
commit f827829afe
38 changed files with 596 additions and 46 deletions
+2
View File
@@ -69,6 +69,7 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
}()
api.AGPL.Options.SetUserGroups = api.setUserGroups
api.AGPL.Options.SetUserSiteRoles = api.setUserSiteRoles
api.AGPL.SiteHandler.AppearanceFetcher = api.fetchAppearanceConfig
api.AGPL.SiteHandler.RegionsFetcher = func(ctx context.Context) (any, error) {
// If the user can read the workspace proxy resource, return that.
@@ -405,6 +406,7 @@ func (api *API) updateEntitlements(ctx context.Context) error {
// FeatureAdvancedTemplateScheduling.
codersdk.FeatureTemplateRestartRequirement: api.DefaultQuietHoursSchedule != "",
codersdk.FeatureWorkspaceProxy: true,
codersdk.FeatureUserRoleManagement: true,
})
if err != nil {
return err
+1
View File
@@ -56,6 +56,7 @@ func TestEntitlements(t *testing.T) {
codersdk.FeatureExternalProvisionerDaemons: 1,
codersdk.FeatureAdvancedTemplateScheduling: 1,
codersdk.FeatureWorkspaceProxy: 1,
codersdk.FeatureUserRoleManagement: 1,
},
GraceAt: time.Now().Add(59 * 24 * time.Hour),
})
+1
View File
@@ -416,6 +416,7 @@ func convertUser(user database.User, organizationIDs []uuid.UUID) codersdk.User
OrganizationIDs: organizationIDs,
Roles: make([]codersdk.Role, 0, len(user.RBACRoles)),
AvatarURL: user.AvatarURL.String,
LoginType: codersdk.LoginType(user.LoginType),
}
for _, roleName := range user.RBACRoles {
+28
View File
@@ -6,6 +6,8 @@ import (
"github.com/google/uuid"
"golang.org/x/xerrors"
"cdr.dev/slog"
"github.com/coder/coder/coderd"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/codersdk"
)
@@ -50,3 +52,29 @@ func (api *API) setUserGroups(ctx context.Context, db database.Store, userID uui
return nil
}, nil)
}
func (api *API) setUserSiteRoles(ctx context.Context, db database.Store, userID uuid.UUID, roles []string) error {
api.entitlementsMu.RLock()
enabled := api.entitlements.Features[codersdk.FeatureUserRoleManagement].Enabled
api.entitlementsMu.RUnlock()
if !enabled {
api.Logger.Warn(ctx, "attempted to assign OIDC user roles without enterprise entitlement, roles left unchanged",
slog.F("user_id", userID), slog.F("roles", roles),
)
return nil
}
// Should this be feature protected?
return db.InTx(func(tx database.Store) error {
_, err := coderd.UpdateSiteUserRoles(ctx, db, database.UpdateUserRolesParams{
GrantedRoles: roles,
ID: userID,
})
if err != nil {
return xerrors.Errorf("set user roles(%s): %w", userID.String(), err)
}
return nil
}, nil)
}
+94
View File
@@ -16,6 +16,7 @@ import (
"github.com/coder/coder/coderd"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/rbac"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/enterprise/coderd/coderdenttest"
"github.com/coder/coder/testutil"
@@ -24,6 +25,99 @@ import (
// nolint:bodyclose
func TestUserOIDC(t *testing.T) {
t.Parallel()
t.Run("RoleSync", func(t *testing.T) {
t.Parallel()
t.Run("NewUserAndRemoveRoles", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitMedium)
conf := coderdtest.NewOIDCConfig(t, "")
oidcRoleName := "TemplateAuthor"
config := conf.OIDCConfig(t, jwt.MapClaims{}, func(cfg *coderd.OIDCConfig) {
cfg.UserRoleMapping = map[string][]string{oidcRoleName: {rbac.RoleTemplateAdmin(), rbac.RoleUserAdmin()}}
})
config.AllowSignups = true
config.UserRoleField = "roles"
client, _ := coderdenttest.New(t, &coderdenttest.Options{
Options: &coderdtest.Options{
OIDCConfig: config,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{codersdk.FeatureUserRoleManagement: 1},
},
})
admin, err := client.User(ctx, "me")
require.NoError(t, err)
require.Len(t, admin.OrganizationIDs, 1)
resp := oidcCallback(t, client, conf.EncodeClaims(t, jwt.MapClaims{
"email": "alice@coder.com",
"roles": []string{"random", oidcRoleName, rbac.RoleOwner()},
}))
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
user, err := client.User(ctx, "alice")
require.NoError(t, err)
require.Len(t, user.Roles, 3)
roleNames := []string{user.Roles[0].Name, user.Roles[1].Name, user.Roles[2].Name}
require.ElementsMatch(t, roleNames, []string{rbac.RoleTemplateAdmin(), rbac.RoleUserAdmin(), rbac.RoleOwner()})
// Now remove the roles with a new oidc login
resp = oidcCallback(t, client, conf.EncodeClaims(t, jwt.MapClaims{
"email": "alice@coder.com",
"roles": []string{"random"},
}))
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
user, err = client.User(ctx, "alice")
require.NoError(t, err)
require.Len(t, user.Roles, 0)
})
t.Run("BlockAssignRoles", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitMedium)
conf := coderdtest.NewOIDCConfig(t, "")
config := conf.OIDCConfig(t, jwt.MapClaims{})
config.AllowSignups = true
config.UserRoleField = "roles"
client, _ := coderdenttest.New(t, &coderdenttest.Options{
Options: &coderdtest.Options{
OIDCConfig: config,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{codersdk.FeatureUserRoleManagement: 1},
},
})
admin, err := client.User(ctx, "me")
require.NoError(t, err)
require.Len(t, admin.OrganizationIDs, 1)
resp := oidcCallback(t, client, conf.EncodeClaims(t, jwt.MapClaims{
"email": "alice@coder.com",
"roles": []string{},
}))
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
// Try to manually update user roles, even though controlled by oidc
// role sync.
_, err = client.UpdateUserRoles(ctx, "alice", codersdk.UpdateRoles{
Roles: []string{
rbac.RoleTemplateAdmin(),
},
})
require.Error(t, err)
require.ErrorContains(t, err, "Cannot modify roles for OIDC users when role sync is enabled.")
})
})
t.Run("Groups", func(t *testing.T) {
t.Parallel()
t.Run("Assigns", func(t *testing.T) {