chore: move organizatinon sync to runtime configuration (#15431)

Moves the configuration from environment to database backed, to allow
configuring organization sync at runtime.
This commit is contained in:
Steven Masley
2024-11-08 08:44:14 -06:00
committed by GitHub
parent 7b33ab0dcf
commit 782214bcd8
28 changed files with 883 additions and 280 deletions
+109
View File
@@ -3166,6 +3166,9 @@ const docTemplate = `{
"CoderSessionToken": []
}
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
@@ -3182,6 +3185,15 @@ const docTemplate = `{
"name": "organization",
"in": "path",
"required": true
},
{
"description": "New settings",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/codersdk.GroupSyncSettings"
}
}
],
"responses": {
@@ -3234,6 +3246,9 @@ const docTemplate = `{
"CoderSessionToken": []
}
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
@@ -3250,6 +3265,15 @@ const docTemplate = `{
"name": "organization",
"in": "path",
"required": true
},
{
"description": "New settings",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/codersdk.RoleSyncSettings"
}
}
],
"responses": {
@@ -3770,6 +3794,68 @@ const docTemplate = `{
}
}
},
"/settings/idpsync/organization": {
"get": {
"security": [
{
"CoderSessionToken": []
}
],
"produces": [
"application/json"
],
"tags": [
"Enterprise"
],
"summary": "Get organization IdP Sync settings",
"operationId": "get-organization-idp-sync-settings",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/codersdk.OrganizationSyncSettings"
}
}
}
},
"patch": {
"security": [
{
"CoderSessionToken": []
}
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Enterprise"
],
"summary": "Update organization IdP Sync settings",
"operationId": "update-organization-idp-sync-settings",
"parameters": [
{
"description": "New settings",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/codersdk.OrganizationSyncSettings"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/codersdk.OrganizationSyncSettings"
}
}
}
}
},
"/tailnet": {
"get": {
"security": [
@@ -11807,6 +11893,29 @@ const docTemplate = `{
}
}
},
"codersdk.OrganizationSyncSettings": {
"type": "object",
"properties": {
"field": {
"description": "Field selects the claim field to be used as the created user's\norganizations. If the field is the empty string, then no organization\nupdates will ever come from the OIDC provider.",
"type": "string"
},
"mapping": {
"description": "Mapping maps from an OIDC claim --\u003e Coder organization uuid",
"type": "object",
"additionalProperties": {
"type": "array",
"items": {
"type": "string"
}
}
},
"organization_assign_default": {
"description": "AssignDefault will ensure the default org is always included\nfor every user, regardless of their claims. This preserves legacy behavior.",
"type": "boolean"
}
}
},
"codersdk.PatchGroupRequest": {
"type": "object",
"properties": {
+95
View File
@@ -2784,6 +2784,7 @@
"CoderSessionToken": []
}
],
"consumes": ["application/json"],
"produces": ["application/json"],
"tags": ["Enterprise"],
"summary": "Update group IdP Sync settings by organization",
@@ -2796,6 +2797,15 @@
"name": "organization",
"in": "path",
"required": true
},
{
"description": "New settings",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/codersdk.GroupSyncSettings"
}
}
],
"responses": {
@@ -2844,6 +2854,7 @@
"CoderSessionToken": []
}
],
"consumes": ["application/json"],
"produces": ["application/json"],
"tags": ["Enterprise"],
"summary": "Update role IdP Sync settings by organization",
@@ -2856,6 +2867,15 @@
"name": "organization",
"in": "path",
"required": true
},
{
"description": "New settings",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/codersdk.RoleSyncSettings"
}
}
],
"responses": {
@@ -3316,6 +3336,58 @@
}
}
},
"/settings/idpsync/organization": {
"get": {
"security": [
{
"CoderSessionToken": []
}
],
"produces": ["application/json"],
"tags": ["Enterprise"],
"summary": "Get organization IdP Sync settings",
"operationId": "get-organization-idp-sync-settings",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/codersdk.OrganizationSyncSettings"
}
}
}
},
"patch": {
"security": [
{
"CoderSessionToken": []
}
],
"consumes": ["application/json"],
"produces": ["application/json"],
"tags": ["Enterprise"],
"summary": "Update organization IdP Sync settings",
"operationId": "update-organization-idp-sync-settings",
"parameters": [
{
"description": "New settings",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/codersdk.OrganizationSyncSettings"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/codersdk.OrganizationSyncSettings"
}
}
}
}
},
"/tailnet": {
"get": {
"security": [
@@ -10641,6 +10713,29 @@
}
}
},
"codersdk.OrganizationSyncSettings": {
"type": "object",
"properties": {
"field": {
"description": "Field selects the claim field to be used as the created user's\norganizations. If the field is the empty string, then no organization\nupdates will ever come from the OIDC provider.",
"type": "string"
},
"mapping": {
"description": "Mapping maps from an OIDC claim --\u003e Coder organization uuid",
"type": "object",
"additionalProperties": {
"type": "array",
"items": {
"type": "string"
}
}
},
"organization_assign_default": {
"description": "AssignDefault will ensure the default org is always included\nfor every user, regardless of their claims. This preserves legacy behavior.",
"type": "boolean"
}
}
},
"codersdk.PatchGroupRequest": {
"type": "object",
"properties": {
+2 -3
View File
@@ -33,9 +33,8 @@ var _ database.Store = (*querier)(nil)
const wrapname = "dbauthz.querier"
// NoActorError wraps ErrNoRows for the api to return a 404. This is the correct
// response when the user is not authorized.
var NoActorError = xerrors.Errorf("no authorization actor in context: %w", sql.ErrNoRows)
// NoActorError is returned if no actor is present in the context.
var NoActorError = xerrors.Errorf("no authorization actor in context")
// NotAuthorizedError is a sentinel error that unwraps to sql.ErrNoRows.
// This allows the internal error to be read by the caller if needed. Otherwise
+5 -5
View File
@@ -20,12 +20,12 @@ import (
)
type GroupParams struct {
// SyncEnabled if false will skip syncing the user's groups
SyncEnabled bool
// SyncEntitled if false will skip syncing the user's groups
SyncEntitled bool
MergedClaims jwt.MapClaims
}
func (AGPLIDPSync) GroupSyncEnabled() bool {
func (AGPLIDPSync) GroupSyncEntitled() bool {
// AGPL does not support syncing groups.
return false
}
@@ -73,13 +73,13 @@ func (s AGPLIDPSync) GroupSyncSettings(ctx context.Context, orgID uuid.UUID, db
func (s AGPLIDPSync) ParseGroupClaims(_ context.Context, _ jwt.MapClaims) (GroupParams, *HTTPError) {
return GroupParams{
SyncEnabled: s.GroupSyncEnabled(),
SyncEntitled: s.GroupSyncEntitled(),
}, nil
}
func (s AGPLIDPSync) SyncGroups(ctx context.Context, db database.Store, user database.User, params GroupParams) error {
// Nothing happens if sync is not enabled
if !params.SyncEnabled {
if !params.SyncEntitled {
return nil
}
+5 -5
View File
@@ -41,7 +41,7 @@ func TestParseGroupClaims(t *testing.T) {
params, err := s.ParseGroupClaims(ctx, jwt.MapClaims{})
require.Nil(t, err)
require.False(t, params.SyncEnabled)
require.False(t, params.SyncEntitled)
})
// AllowList has no effect in AGPL
@@ -61,7 +61,7 @@ func TestParseGroupClaims(t *testing.T) {
params, err := s.ParseGroupClaims(ctx, jwt.MapClaims{})
require.Nil(t, err)
require.False(t, params.SyncEnabled)
require.False(t, params.SyncEntitled)
})
}
@@ -276,7 +276,7 @@ func TestGroupSyncTable(t *testing.T) {
// Do the group sync!
err := s.SyncGroups(ctx, db, user, idpsync.GroupParams{
SyncEnabled: true,
SyncEntitled: true,
MergedClaims: userClaims,
})
require.NoError(t, err)
@@ -363,7 +363,7 @@ func TestGroupSyncTable(t *testing.T) {
// Do the group sync!
err = s.SyncGroups(ctx, db, user, idpsync.GroupParams{
SyncEnabled: true,
SyncEntitled: true,
MergedClaims: userClaims,
})
require.NoError(t, err)
@@ -420,7 +420,7 @@ func TestSyncDisabled(t *testing.T) {
// Do the group sync!
err := s.SyncGroups(ctx, db, user, idpsync.GroupParams{
SyncEnabled: false,
SyncEntitled: false,
MergedClaims: jwt.MapClaims{
"groups": []string{"baz", "bop"},
},
+12 -5
View File
@@ -24,8 +24,13 @@ import (
// claims to the internal representation of a user in Coder.
// TODO: Move group + role sync into this interface.
type IDPSync interface {
AssignDefaultOrganization() bool
OrganizationSyncEnabled() bool
OrganizationSyncEntitled() bool
OrganizationSyncSettings(ctx context.Context, db database.Store) (*OrganizationSyncSettings, error)
UpdateOrganizationSettings(ctx context.Context, db database.Store, settings OrganizationSyncSettings) error
// OrganizationSyncEnabled returns true if all OIDC users are assigned
// to organizations via org sync settings.
// This is used to know when to disable manual org membership assignment.
OrganizationSyncEnabled(ctx context.Context, db database.Store) bool
// ParseOrganizationClaims takes claims from an OIDC provider, and returns the
// organization sync params for assigning users into organizations.
ParseOrganizationClaims(ctx context.Context, mergedClaims jwt.MapClaims) (OrganizationParams, *HTTPError)
@@ -33,7 +38,7 @@ type IDPSync interface {
// provided params.
SyncOrganizations(ctx context.Context, tx database.Store, user database.User, params OrganizationParams) error
GroupSyncEnabled() bool
GroupSyncEntitled() bool
// ParseGroupClaims takes claims from an OIDC provider, and returns the params
// for group syncing. Most of the logic happens in SyncGroups.
ParseGroupClaims(ctx context.Context, mergedClaims jwt.MapClaims) (GroupParams, *HTTPError)
@@ -147,8 +152,9 @@ func FromDeploymentValues(dv *codersdk.DeploymentValues) DeploymentSyncSettings
type SyncSettings struct {
DeploymentSyncSettings
Group runtimeconfig.RuntimeEntry[*GroupSyncSettings]
Role runtimeconfig.RuntimeEntry[*RoleSyncSettings]
Group runtimeconfig.RuntimeEntry[*GroupSyncSettings]
Role runtimeconfig.RuntimeEntry[*RoleSyncSettings]
Organization runtimeconfig.RuntimeEntry[*OrganizationSyncSettings]
}
func NewAGPLSync(logger slog.Logger, manager *runtimeconfig.Manager, settings DeploymentSyncSettings) *AGPLIDPSync {
@@ -159,6 +165,7 @@ func NewAGPLSync(logger slog.Logger, manager *runtimeconfig.Manager, settings De
DeploymentSyncSettings: settings,
Group: runtimeconfig.MustNew[*GroupSyncSettings]("group-sync-settings"),
Role: runtimeconfig.MustNew[*RoleSyncSettings]("role-sync-settings"),
Organization: runtimeconfig.MustNew[*OrganizationSyncSettings]("organization-sync-settings"),
},
}
}
+117 -29
View File
@@ -3,6 +3,7 @@ package idpsync
import (
"context"
"database/sql"
"encoding/json"
"github.com/golang-jwt/jwt/v4"
"github.com/google/uuid"
@@ -13,35 +14,59 @@ import (
"github.com/coder/coder/v2/coderd/database/db2sdk"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/runtimeconfig"
"github.com/coder/coder/v2/coderd/util/slice"
)
type OrganizationParams struct {
// SyncEnabled if false will skip syncing the user's organizations.
SyncEnabled bool
// IncludeDefault is primarily for single org deployments. It will ensure
// a user is always inserted into the default org.
IncludeDefault bool
// Organizations is the list of organizations the user should be a member of
// assuming syncing is turned on.
Organizations []uuid.UUID
// SyncEntitled if false will skip syncing the user's organizations.
SyncEntitled bool
// MergedClaims are passed to the organization level for syncing
MergedClaims jwt.MapClaims
}
func (AGPLIDPSync) OrganizationSyncEnabled() bool {
func (AGPLIDPSync) OrganizationSyncEntitled() bool {
// AGPL does not support syncing organizations.
return false
}
func (s AGPLIDPSync) AssignDefaultOrganization() bool {
return s.OrganizationAssignDefault
func (AGPLIDPSync) OrganizationSyncEnabled(_ context.Context, _ database.Store) bool {
return false
}
func (s AGPLIDPSync) ParseOrganizationClaims(_ context.Context, _ jwt.MapClaims) (OrganizationParams, *HTTPError) {
func (s AGPLIDPSync) UpdateOrganizationSettings(ctx context.Context, db database.Store, settings OrganizationSyncSettings) error {
rlv := s.Manager.Resolver(db)
err := s.SyncSettings.Organization.SetRuntimeValue(ctx, rlv, &settings)
if err != nil {
return xerrors.Errorf("update organization sync settings: %w", err)
}
return nil
}
func (s AGPLIDPSync) OrganizationSyncSettings(ctx context.Context, db database.Store) (*OrganizationSyncSettings, error) {
rlv := s.Manager.Resolver(db)
orgSettings, err := s.SyncSettings.Organization.Resolve(ctx, rlv)
if err != nil {
if !xerrors.Is(err, runtimeconfig.ErrEntryNotFound) {
return nil, xerrors.Errorf("resolve org sync settings: %w", err)
}
// Default to the statically assigned settings if they exist.
orgSettings = &OrganizationSyncSettings{
Field: s.DeploymentSyncSettings.OrganizationField,
Mapping: s.DeploymentSyncSettings.OrganizationMapping,
AssignDefault: s.DeploymentSyncSettings.OrganizationAssignDefault,
}
}
return orgSettings, nil
}
func (s AGPLIDPSync) ParseOrganizationClaims(_ context.Context, claims jwt.MapClaims) (OrganizationParams, *HTTPError) {
// For AGPL we only sync the default organization.
return OrganizationParams{
SyncEnabled: s.OrganizationSyncEnabled(),
IncludeDefault: s.OrganizationAssignDefault,
Organizations: []uuid.UUID{},
SyncEntitled: s.OrganizationSyncEntitled(),
MergedClaims: claims,
}, nil
}
@@ -49,21 +74,25 @@ func (s AGPLIDPSync) ParseOrganizationClaims(_ context.Context, _ jwt.MapClaims)
// organizations. It will add and remove their membership to match the expected set.
func (s AGPLIDPSync) SyncOrganizations(ctx context.Context, tx database.Store, user database.User, params OrganizationParams) error {
// Nothing happens if sync is not enabled
if !params.SyncEnabled {
if !params.SyncEntitled {
return nil
}
// nolint:gocritic // all syncing is done as a system user
ctx = dbauthz.AsSystemRestricted(ctx)
// This is a bit hacky, but if AssignDefault is included, then always
// make sure to include the default org in the list of expected.
if s.OrganizationAssignDefault {
defaultOrg, err := tx.GetDefaultOrganization(ctx)
if err != nil {
return xerrors.Errorf("failed to get default organization: %w", err)
}
params.Organizations = append(params.Organizations, defaultOrg.ID)
orgSettings, err := s.OrganizationSyncSettings(ctx, tx)
if err != nil {
return xerrors.Errorf("failed to get org sync settings: %w", err)
}
if orgSettings.Field == "" {
return nil // No sync configured, nothing to do
}
expectedOrgs, err := orgSettings.ParseClaims(ctx, tx, params.MergedClaims)
if err != nil {
return xerrors.Errorf("organization claims: %w", err)
}
existingOrgs, err := tx.GetOrganizationsByUserID(ctx, user.ID)
@@ -77,11 +106,10 @@ func (s AGPLIDPSync) SyncOrganizations(ctx context.Context, tx database.Store, u
// Find the difference in the expected and the existing orgs, and
// correct the set of orgs the user is a member of.
add, remove := slice.SymmetricDifference(existingOrgIDs, params.Organizations)
add, remove := slice.SymmetricDifference(existingOrgIDs, expectedOrgs)
notExists := make([]uuid.UUID, 0)
for _, orgID := range add {
//nolint:gocritic // System actor being used to assign orgs
_, err := tx.InsertOrganizationMember(dbauthz.AsSystemRestricted(ctx), database.InsertOrganizationMemberParams{
_, err := tx.InsertOrganizationMember(ctx, database.InsertOrganizationMemberParams{
OrganizationID: orgID,
UserID: user.ID,
CreatedAt: dbtime.Now(),
@@ -98,8 +126,7 @@ func (s AGPLIDPSync) SyncOrganizations(ctx context.Context, tx database.Store, u
}
for _, orgID := range remove {
//nolint:gocritic // System actor being used to assign orgs
err := tx.DeleteOrganizationMember(dbauthz.AsSystemRestricted(ctx), database.DeleteOrganizationMemberParams{
err := tx.DeleteOrganizationMember(ctx, database.DeleteOrganizationMemberParams{
OrganizationID: orgID,
UserID: user.ID,
})
@@ -117,3 +144,64 @@ func (s AGPLIDPSync) SyncOrganizations(ctx context.Context, tx database.Store, u
}
return nil
}
type OrganizationSyncSettings struct {
// Field selects the claim field to be used as the created user's
// organizations. If the field is the empty string, then no organization updates
// will ever come from the OIDC provider.
Field string
// Mapping controls how organizations returned by the OIDC provider get mapped
Mapping map[string][]uuid.UUID
// AssignDefault will ensure all users that authenticate will be
// placed into the default organization. This is mostly a hack to support
// legacy deployments.
AssignDefault bool
}
func (s *OrganizationSyncSettings) Set(v string) error {
return json.Unmarshal([]byte(v), s)
}
func (s *OrganizationSyncSettings) String() string {
return runtimeconfig.JSONString(s)
}
// ParseClaims will parse the claims and return the list of organizations the user
// should sync to.
func (s *OrganizationSyncSettings) ParseClaims(ctx context.Context, db database.Store, mergedClaims jwt.MapClaims) ([]uuid.UUID, error) {
userOrganizations := make([]uuid.UUID, 0)
if s.AssignDefault {
// This is a bit hacky, but if AssignDefault is included, then always
// make sure to include the default org in the list of expected.
defaultOrg, err := db.GetDefaultOrganization(ctx)
if err != nil {
return nil, xerrors.Errorf("failed to get default organization: %w", err)
}
// Always include default org.
userOrganizations = append(userOrganizations, defaultOrg.ID)
}
organizationRaw, ok := mergedClaims[s.Field]
if !ok {
return userOrganizations, nil
}
parsedOrganizations, err := ParseStringSliceClaim(organizationRaw)
if err != nil {
return userOrganizations, xerrors.Errorf("failed to parese organizations OIDC claims: %w", err)
}
// add any mapped organizations
for _, parsedOrg := range parsedOrganizations {
if mappedOrganization, ok := s.Mapping[parsedOrg]; ok {
// parsedOrg is in the mapping, so add the mapped organizations to the
// user's organizations.
userOrganizations = append(userOrganizations, mappedOrganization...)
}
}
// Deduplicate the organizations
return slice.Unique(userOrganizations), nil
}
+1 -24
View File
@@ -16,27 +16,6 @@ import (
func TestParseOrganizationClaims(t *testing.T) {
t.Parallel()
t.Run("SingleOrgDeployment", func(t *testing.T) {
t.Parallel()
s := idpsync.NewAGPLSync(slogtest.Make(t, &slogtest.Options{}),
runtimeconfig.NewManager(),
idpsync.DeploymentSyncSettings{
OrganizationField: "",
OrganizationMapping: nil,
OrganizationAssignDefault: true,
})
ctx := testutil.Context(t, testutil.WaitMedium)
params, err := s.ParseOrganizationClaims(ctx, jwt.MapClaims{})
require.Nil(t, err)
require.Empty(t, params.Organizations)
require.True(t, params.IncludeDefault)
require.False(t, params.SyncEnabled)
})
t.Run("AGPL", func(t *testing.T) {
t.Parallel()
@@ -56,8 +35,6 @@ func TestParseOrganizationClaims(t *testing.T) {
params, err := s.ParseOrganizationClaims(ctx, jwt.MapClaims{})
require.Nil(t, err)
require.Empty(t, params.Organizations)
require.False(t, params.IncludeDefault)
require.False(t, params.SyncEnabled)
require.False(t, params.SyncEntitled)
})
}
+24 -6
View File
@@ -45,11 +45,7 @@ func (api *API) postOrganizationMember(rw http.ResponseWriter, r *http.Request)
aReq.Old = database.AuditableOrganizationMember{}
defer commitAudit()
if user.LoginType == database.LoginTypeOIDC && api.IDPSync.OrganizationSyncEnabled() {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Organization sync is enabled for OIDC users, meaning manual organization assignment is not allowed for this user.",
Detail: fmt.Sprintf("User %s is an OIDC user and organization sync is enabled. Ask an administrator to resolve this in your external IDP.", user.ID),
})
if !api.manualOrganizationMembership(ctx, rw, user) {
return
}
@@ -116,6 +112,14 @@ func (api *API) deleteOrganizationMember(rw http.ResponseWriter, r *http.Request
aReq.Old = member.OrganizationMember.Auditable(member.Username)
defer commitAudit()
// Note: we disallow adding OIDC users if organization sync is enabled.
// For removing members, do not have this same enforcement. As long as a user
// does not re-login, they will not be immediately removed from the organization.
// There might be an urgent need to revoke access.
// A user can re-login if they are removed in error.
// If we add a feature to force logout a user, then we can prevent manual
// member removal when organization sync is enabled, and use force logout instead.
if member.UserID == apiKey.UserID {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{Message: "cannot remove self from an organization"})
return
@@ -272,7 +276,7 @@ func (api *API) allowChangingMemberRoles(ctx context.Context, rw http.ResponseWr
}
if orgSync {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Cannot modify roles for OIDC users when role sync is enabled. This organization member's roles are managed by the identity provider.",
Message: "Cannot modify roles for OIDC users when role sync is enabled. This organization member's roles are managed by the identity provider. Have the user re-login to refresh their roles.",
Detail: "'User Role Field' is set in the organization settings. Ask an administrator to adjust or disable these settings.",
})
return false
@@ -372,3 +376,17 @@ func convertOrganizationMembersWithUserData(ctx context.Context, db database.Sto
return converted, nil
}
// manualOrganizationMembership checks if the user is an OIDC user and if organization sync is enabled.
// If organization sync is enabled, manual organization assignment is not allowed,
// since all organization membership is controlled by the external IDP.
func (api *API) manualOrganizationMembership(ctx context.Context, rw http.ResponseWriter, user database.User) bool {
if user.LoginType == database.LoginTypeOIDC && api.IDPSync.OrganizationSyncEnabled(ctx, api.Database) {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Organization sync is enabled for OIDC users, meaning manual organization assignment is not allowed for this user. Have the user re-login to refresh their organizations.",
Detail: fmt.Sprintf("User %s is an OIDC user and organization sync is enabled. Ask an administrator to resolve the membership in your external IDP.", user.Username),
})
return false
}
return true
}
+2
View File
@@ -352,6 +352,8 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
ResourceOrganizationMember.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
ResourceGroup.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
ResourceGroupMember.Type: {policy.ActionRead},
// Manage org membership based on OIDC claims
ResourceIdpsyncSettings.Type: {policy.ActionRead, policy.ActionUpdate},
}),
Org: map[string][]Permission{},
User: []Permission{},
+17 -2
View File
@@ -733,10 +733,25 @@ func TestRolePermissions(t *testing.T) {
Actions: []policy.Action{policy.ActionRead, policy.ActionUpdate},
Resource: rbac.ResourceIdpsyncSettings.InOrg(orgID),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgAdmin, orgUserAdmin},
true: {owner, orgAdmin, orgUserAdmin, userAdmin},
false: {
orgMemberMe, otherOrgAdmin,
memberMe, userAdmin, templateAdmin,
memberMe, templateAdmin,
orgAuditor, orgTemplateAdmin,
otherOrgMember, otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin,
},
},
},
{
Name: "OrganizationIDPSyncSettings",
Actions: []policy.Action{policy.ActionRead, policy.ActionUpdate},
Resource: rbac.ResourceIdpsyncSettings,
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, userAdmin},
false: {
orgAdmin, orgUserAdmin,
orgMemberMe, otherOrgAdmin,
memberMe, templateAdmin,
orgAuditor, orgTemplateAdmin,
otherOrgMember, otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin,
},
+13 -20
View File
@@ -968,12 +968,10 @@ func (api *API) userOAuth2Github(rw http.ResponseWriter, r *http.Request) {
Name: normName,
DebugContext: OauthDebugContext{},
GroupSync: idpsync.GroupParams{
SyncEnabled: false,
SyncEntitled: false,
},
OrganizationSync: idpsync.OrganizationParams{
SyncEnabled: false,
IncludeDefault: true,
Organizations: []uuid.UUID{},
SyncEntitled: false,
},
}).SetInitAuditRequest(func(params *audit.RequestParams) (*audit.Request[database.User], func()) {
return audit.InitRequest[database.User](rw, params)
@@ -1513,14 +1511,6 @@ func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.C
// This can happen if a user is a built-in user but is signing in
// with OIDC for the first time.
if user.ID == uuid.Nil {
// Until proper multi-org support, all users will be added to the default organization.
// The default organization should always be present.
//nolint:gocritic
defaultOrganization, err := tx.GetDefaultOrganization(dbauthz.AsSystemRestricted(ctx))
if err != nil {
return xerrors.Errorf("unable to fetch default organization: %w", err)
}
//nolint:gocritic
_, err = tx.GetUserByEmailOrUsername(dbauthz.AsSystemRestricted(ctx), database.GetUserByEmailOrUsernameParams{
Username: params.Username,
@@ -1555,19 +1545,22 @@ func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.C
}
}
// Even if org sync is disabled, single org deployments will always
// have this set to true.
orgIDs := []uuid.UUID{}
if params.OrganizationSync.IncludeDefault {
orgIDs = append(orgIDs, defaultOrganization.ID)
//nolint:gocritic
defaultOrganization, err := tx.GetDefaultOrganization(dbauthz.AsSystemRestricted(ctx))
if err != nil {
return xerrors.Errorf("unable to fetch default organization: %w", err)
}
//nolint:gocritic
user, err = api.CreateUser(dbauthz.AsSystemRestricted(ctx), tx, CreateUserRequest{
CreateUserRequestWithOrgs: codersdk.CreateUserRequestWithOrgs{
Email: params.Email,
Username: params.Username,
OrganizationIDs: orgIDs,
Email: params.Email,
Username: params.Username,
// This is a kludge, but all users are defaulted into the default
// organization. This exists as the default behavior.
// If org sync is enabled and configured, the user's groups
// will change based on the org sync settings.
OrganizationIDs: []uuid.UUID{defaultOrganization.ID},
UserStatus: ptr.Ref(codersdk.UserStatusActive),
},
LoginType: params.LoginType,