Skip to content

Commit 1667624

Browse files
committed
chore: refactor into agpl and enterprise
1 parent 2db71b3 commit 1667624

File tree

10 files changed

+219
-93
lines changed

10 files changed

+219
-93
lines changed

cli/server.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import (
5555

5656
"cdr.dev/slog"
5757
"cdr.dev/slog/sloggers/sloghuman"
58+
"github.com/coder/coder/v2/coderd/idpsync"
5859
"github.com/coder/pretty"
5960
"github.com/coder/quartz"
6061
"github.com/coder/retry"
@@ -197,6 +198,11 @@ func createOIDCConfig(ctx context.Context, logger slog.Logger, vals *codersdk.De
197198
SignupsDisabledText: vals.OIDC.SignupsDisabledText.String(),
198199
IconURL: vals.OIDC.IconURL.String(),
199200
IgnoreEmailVerified: vals.OIDC.IgnoreEmailVerified.Value(),
201+
IDPSync: idpsync.NewSync(logger, idpsync.SyncSettings{
202+
OrganizationField: vals.OIDC.OrganizationField.Value(),
203+
OrganizationMapping: vals.OIDC.OrganizationMapping.Value,
204+
OrganizationAssignDefault: vals.OIDC.OrganizationAssignDefault.Value,
205+
}),
200206
}, nil
201207
}
202208

coderd/idpsync/idpsync.go

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,54 @@
11
package idpsync
22

33
import (
4+
"context"
45
"net/http"
56
"strings"
67

78
"github.com/google/uuid"
89
"golang.org/x/xerrors"
910

1011
"cdr.dev/slog"
11-
"github.com/coder/coder/v2/coderd/entitlements"
12+
"github.com/coder/coder/v2/coderd/database"
1213
"github.com/coder/coder/v2/coderd/httpapi"
1314
"github.com/coder/coder/v2/codersdk"
1415
"github.com/coder/coder/v2/site"
1516
)
1617

17-
// IDPSync is the configuration for syncing user information from an external
18+
type IDPSync interface {
19+
// ParseOrganizationClaims takes claims from an OIDC provider, and returns the
20+
// organization sync params for assigning users into organizations.
21+
ParseOrganizationClaims(ctx context.Context, _ map[string]interface{}) (OrganizationParams, *HttpError)
22+
// SyncOrganizations assigns and removed users from organizations based on the
23+
// provided params.
24+
SyncOrganizations(ctx context.Context, tx database.Store, user database.User, params OrganizationParams) error
25+
}
26+
27+
// AGPLIDPSync is the configuration for syncing user information from an external
1828
// IDP. All related code to syncing user information should be in this package.
19-
type IDPSync struct {
20-
logger slog.Logger
21-
entitlements *entitlements.Set
29+
type AGPLIDPSync struct {
30+
Logger slog.Logger
31+
32+
SyncSettings
33+
}
2234

35+
type SyncSettings struct {
2336
// OrganizationField selects the claim field to be used as the created user's
2437
// organizations. If the field is the empty string, then no organization updates
2538
// will ever come from the OIDC provider.
2639
OrganizationField string
2740
// OrganizationMapping controls how organizations returned by the OIDC provider get mapped
2841
OrganizationMapping map[string][]uuid.UUID
29-
// OrganizationAlwaysAssign will ensure all users that authenticate will be
30-
// placed into the specified organizations.
31-
OrganizationAlwaysAssign []uuid.UUID
42+
// OrganizationAssignDefault will ensure all users that authenticate will be
43+
// placed into the default organization. This is mostly a hack to support
44+
// legacy deployments.
45+
OrganizationAssignDefault bool
3246
}
3347

34-
func NewSync(logger slog.Logger, set *entitlements.Set) *IDPSync {
35-
return &IDPSync{
36-
logger: logger.Named("idp-sync"),
37-
entitlements: set,
48+
func NewSync(logger slog.Logger, settings SyncSettings) *AGPLIDPSync {
49+
return &AGPLIDPSync{
50+
Logger: logger.Named("idp-sync"),
51+
SyncSettings: settings,
3852
}
3953
}
4054

coderd/idpsync/organization.go

Lines changed: 25 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package idpsync
33
import (
44
"context"
55
"database/sql"
6-
"net/http"
76

87
"github.com/google/uuid"
98
"golang.org/x/xerrors"
@@ -16,64 +15,46 @@ import (
1615
"github.com/coder/coder/v2/coderd/util/slice"
1716
)
1817

19-
func (s IDPSync) ParseOrganizationClaims(ctx context.Context, mergedClaims map[string]interface{}) (OrganizationParams, *HttpError) {
18+
func (s AGPLIDPSync) ParseOrganizationClaims(ctx context.Context, _ map[string]interface{}) (OrganizationParams, *HttpError) {
2019
// nolint:gocritic // all syncing is done as a system user
2120
ctx = dbauthz.AsSystemRestricted(ctx)
2221

23-
// Copy in the always included static set of organizations.
24-
userOrganizations := make([]uuid.UUID, len(s.OrganizationAlwaysAssign))
25-
copy(userOrganizations, s.OrganizationAlwaysAssign)
26-
27-
// Pull extra organizations from the claims.
28-
if s.OrganizationField != "" {
29-
organizationRaw, ok := mergedClaims[s.OrganizationField]
30-
if ok {
31-
parsedOrganizations, err := ParseStringSliceClaim(organizationRaw)
32-
if err != nil {
33-
return OrganizationParams{}, &HttpError{
34-
Code: http.StatusBadRequest,
35-
Msg: "Failed to sync organizations from the OIDC claims",
36-
Detail: err.Error(),
37-
RenderStaticPage: false,
38-
RenderDetailMarkdown: false,
39-
}
40-
}
41-
42-
// Keep track of which claims are not mapped for debugging purposes.
43-
var ignored []string
44-
for _, parsedOrg := range parsedOrganizations {
45-
if mappedOrganization, ok := s.OrganizationMapping[parsedOrg]; ok {
46-
// parsedOrg is in the mapping, so add the mapped organizations to the
47-
// user's organizations.
48-
userOrganizations = append(userOrganizations, mappedOrganization...)
49-
} else {
50-
ignored = append(ignored, parsedOrg)
51-
}
52-
}
53-
54-
s.logger.Debug(ctx, "parsed organizations from claim",
55-
slog.F("len", len(parsedOrganizations)),
56-
slog.F("ignored", ignored),
57-
slog.F("organizations", parsedOrganizations),
58-
)
59-
}
60-
}
61-
22+
// For AGPL we only rely on 'OrganizationAlwaysAssign'
6223
return OrganizationParams{
63-
Organizations: userOrganizations,
24+
SyncEnabled: false,
25+
IncludeDefault: s.OrganizationAssignDefault,
26+
Organizations: []uuid.UUID{},
6427
}, nil
6528
}
6629

6730
type OrganizationParams struct {
31+
// SyncEnabled if false will skip syncing the user's organizations.
32+
SyncEnabled bool
33+
IncludeDefault bool
6834
// Organizations is the list of organizations the user should be a member of
6935
// assuming syncing is turned on.
7036
Organizations []uuid.UUID
7137
}
7238

73-
func (s IDPSync) SyncOrganizations(ctx context.Context, tx database.Store, user database.User, params OrganizationParams) error {
39+
func (s AGPLIDPSync) SyncOrganizations(ctx context.Context, tx database.Store, user database.User, params OrganizationParams) error {
40+
// Nothing happens if sync is not enabled
41+
if !params.SyncEnabled {
42+
return nil
43+
}
44+
7445
// nolint:gocritic // all syncing is done as a system user
7546
ctx = dbauthz.AsSystemRestricted(ctx)
7647

48+
// This is a bit hacky, but if AssignDefault is included, then always
49+
// make sure to include the default org in the list of expected.
50+
if s.OrganizationAssignDefault {
51+
defaultOrg, err := tx.GetDefaultOrganization(ctx)
52+
if err != nil {
53+
return xerrors.Errorf("failed to get default organization: %w", err)
54+
}
55+
params.Organizations = append(params.Organizations, defaultOrg.ID)
56+
}
57+
7758
existingOrgs, err := tx.GetOrganizationsByUserID(ctx, user.ID)
7859
if err != nil {
7960
return xerrors.Errorf("failed to get user organizations: %w", err)
@@ -117,7 +98,7 @@ func (s IDPSync) SyncOrganizations(ctx context.Context, tx database.Store, user
11798
}
11899

119100
if len(notExists) > 0 {
120-
s.logger.Debug(ctx, "organizations do not exist but attempted to use in org sync",
101+
s.Logger.Debug(ctx, "organizations do not exist but attempted to use in org sync",
121102
slog.F("not_found", notExists),
122103
slog.F("user_id", user.ID),
123104
slog.F("username", user.Username),

coderd/userauth.go

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -738,6 +738,11 @@ type OIDCConfig struct {
738738
// support the userinfo endpoint, or if the userinfo endpoint causes
739739
// undesirable behavior.
740740
IgnoreUserInfo bool
741+
// IDPSync contains all the configuration for syncing user information
742+
// from the external IDP.
743+
IDPSync idpsync.IDPSync
744+
745+
// TODO: Move all idp fields into the IDPSync struct
741746
// GroupField selects the claim field to be used as the created user's
742747
// groups. If the group field is the empty string, then no group updates
743748
// will ever come from the OIDC provider.
@@ -768,16 +773,6 @@ type OIDCConfig struct {
768773
// UserRolesDefault is the default set of roles to assign to a user if role sync
769774
// is enabled.
770775
UserRolesDefault []string
771-
// OrganizationField selects the claim field to be used as the created user's
772-
// organizations. If the field is the empty string, then no organization updates
773-
// will ever come from the OIDC provider.
774-
OrganizationField string
775-
// OrganizationMapping controls how organizations returned by the OIDC provider get mapped
776-
OrganizationMapping map[string][]string
777-
// OrganizationAlwaysAssign will ensure all users that authenticate will be
778-
// placed into the specified organizations. 'default' is a special keyword
779-
// that will use the `IsDefault` organization.
780-
OrganizationAlwaysAssign []string
781776
// SignInText is the text to display on the OIDC login button
782777
SignInText string
783778
// IconURL points to the URL of an icon to display on the OIDC login button

coderd/util/slice/example_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import (
1010
func ExampleSymmetricDifference() {
1111
// The goal of this function is to find the elements to add & remove from
1212
// set 'a' to make it equal to set 'b'.
13-
a := []int{1, 2, 5, 6}
14-
b := []int{2, 3, 4, 5}
13+
a := []int{1, 2, 5, 6, 6, 6}
14+
b := []int{2, 3, 3, 3, 4, 5}
1515
add, remove := slice.SymmetricDifference(a, b)
1616
fmt.Println("Elements to add:", add)
1717
fmt.Println("Elements to remove:", remove)

coderd/util/slice/slice.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,20 @@ func Overlap[T comparable](a []T, b []T) bool {
6262
})
6363
}
6464

65+
func UniqueFunc[T any](a []T, equal func(a, b T) bool) []T {
66+
cpy := make([]T, 0, len(a))
67+
68+
for _, v := range a {
69+
if ContainsCompare(cpy, v, equal) {
70+
continue
71+
}
72+
73+
cpy = append(cpy, v)
74+
}
75+
76+
return cpy
77+
}
78+
6579
// Unique returns a new slice with all duplicate elements removed.
6680
func Unique[T comparable](a []T) []T {
6781
cpy := make([]T, 0, len(a))
@@ -109,15 +123,15 @@ func Descending[T constraints.Ordered](a, b T) int {
109123
}
110124

111125
// SymmetricDifference returns the elements that need to be added and removed
112-
// to get from set 'a' to set 'b'.
126+
// to get from set 'a' to set 'b'. Note that duplicates are ignored in sets.
113127
// In classical set theory notation, SymmetricDifference returns
114128
// all elements of {add} and {remove} together. It is more useful to
115129
// return them as their own slices.
116130
// Notation: A Δ B = (A\B) ∪ (B\A)
117131
// Example:
118132
//
119133
// a := []int{1, 3, 4}
120-
// b := []int{1, 2}
134+
// b := []int{1, 2, 2, 2}
121135
// add, remove := SymmetricDifference(a, b)
122136
// fmt.Println(add) // [2]
123137
// fmt.Println(remove) // [3, 4]
@@ -127,6 +141,8 @@ func SymmetricDifference[T comparable](a, b []T) (add []T, remove []T) {
127141
}
128142

129143
func SymmetricDifferenceFunc[T any](a, b []T, equal func(a, b T) bool) (add []T, remove []T) {
144+
// Ignore all duplicates
145+
a, b = UniqueFunc(a, equal), UniqueFunc(b, equal)
130146
return DifferenceFunc(b, a, equal), DifferenceFunc(a, b, equal)
131147
}
132148

coderd/util/slice/slice_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,22 @@ func TestUnique(t *testing.T) {
5252
slice.Unique([]string{
5353
"a", "a", "a",
5454
}))
55+
56+
require.ElementsMatch(t,
57+
[]int{1, 2, 3, 4, 5},
58+
slice.UniqueFunc([]int{
59+
1, 2, 3, 4, 5, 1, 2, 3, 4, 5,
60+
}, func(a, b int) bool {
61+
return a == b
62+
}))
63+
64+
require.ElementsMatch(t,
65+
[]string{"a"},
66+
slice.UniqueFunc([]string{
67+
"a", "a", "a",
68+
}, func(a, b string) bool {
69+
return a == b
70+
}))
5571
}
5672

5773
func TestContains(t *testing.T) {
@@ -230,4 +246,15 @@ func TestSymmetricDifference(t *testing.T) {
230246
require.ElementsMatch(t, []int{1, 2, 3}, add)
231247
require.ElementsMatch(t, []int{}, remove)
232248
})
249+
250+
t.Run("Duplicates", func(t *testing.T) {
251+
t.Parallel()
252+
253+
add, remove := slice.SymmetricDifference(
254+
[]int{5, 5, 5, 1, 1, 1, 3, 3, 3, 5, 5, 5},
255+
[]int{2, 2, 2, 1, 1, 1, 2, 4, 4, 4, 5, 5, 5, 1, 1},
256+
)
257+
require.ElementsMatch(t, []int{2, 4}, add)
258+
require.ElementsMatch(t, []int{3}, remove)
259+
})
233260
}

codersdk/deployment.go

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"strings"
1515
"time"
1616

17+
"github.com/google/uuid"
1718
"golang.org/x/mod/semver"
1819
"golang.org/x/xerrors"
1920

@@ -512,29 +513,32 @@ type OIDCConfig struct {
512513
ClientID serpent.String `json:"client_id" typescript:",notnull"`
513514
ClientSecret serpent.String `json:"client_secret" typescript:",notnull"`
514515
// ClientKeyFile & ClientCertFile are used in place of ClientSecret for PKI auth.
515-
ClientKeyFile serpent.String `json:"client_key_file" typescript:",notnull"`
516-
ClientCertFile serpent.String `json:"client_cert_file" typescript:",notnull"`
517-
EmailDomain serpent.StringArray `json:"email_domain" typescript:",notnull"`
518-
IssuerURL serpent.String `json:"issuer_url" typescript:",notnull"`
519-
Scopes serpent.StringArray `json:"scopes" typescript:",notnull"`
520-
IgnoreEmailVerified serpent.Bool `json:"ignore_email_verified" typescript:",notnull"`
521-
UsernameField serpent.String `json:"username_field" typescript:",notnull"`
522-
NameField serpent.String `json:"name_field" typescript:",notnull"`
523-
EmailField serpent.String `json:"email_field" typescript:",notnull"`
524-
AuthURLParams serpent.Struct[map[string]string] `json:"auth_url_params" typescript:",notnull"`
525-
IgnoreUserInfo serpent.Bool `json:"ignore_user_info" typescript:",notnull"`
526-
GroupAutoCreate serpent.Bool `json:"group_auto_create" typescript:",notnull"`
527-
GroupRegexFilter serpent.Regexp `json:"group_regex_filter" typescript:",notnull"`
528-
GroupAllowList serpent.StringArray `json:"group_allow_list" typescript:",notnull"`
529-
GroupField serpent.String `json:"groups_field" typescript:",notnull"`
530-
GroupMapping serpent.Struct[map[string]string] `json:"group_mapping" typescript:",notnull"`
531-
UserRoleField serpent.String `json:"user_role_field" typescript:",notnull"`
532-
UserRoleMapping serpent.Struct[map[string][]string] `json:"user_role_mapping" typescript:",notnull"`
533-
UserRolesDefault serpent.StringArray `json:"user_roles_default" typescript:",notnull"`
534-
SignInText serpent.String `json:"sign_in_text" typescript:",notnull"`
535-
IconURL serpent.URL `json:"icon_url" typescript:",notnull"`
536-
SignupsDisabledText serpent.String `json:"signups_disabled_text" typescript:",notnull"`
537-
SkipIssuerChecks serpent.Bool `json:"skip_issuer_checks" typescript:",notnull"`
516+
ClientKeyFile serpent.String `json:"client_key_file" typescript:",notnull"`
517+
ClientCertFile serpent.String `json:"client_cert_file" typescript:",notnull"`
518+
EmailDomain serpent.StringArray `json:"email_domain" typescript:",notnull"`
519+
IssuerURL serpent.String `json:"issuer_url" typescript:",notnull"`
520+
Scopes serpent.StringArray `json:"scopes" typescript:",notnull"`
521+
IgnoreEmailVerified serpent.Bool `json:"ignore_email_verified" typescript:",notnull"`
522+
UsernameField serpent.String `json:"username_field" typescript:",notnull"`
523+
NameField serpent.String `json:"name_field" typescript:",notnull"`
524+
EmailField serpent.String `json:"email_field" typescript:",notnull"`
525+
AuthURLParams serpent.Struct[map[string]string] `json:"auth_url_params" typescript:",notnull"`
526+
IgnoreUserInfo serpent.Bool `json:"ignore_user_info" typescript:",notnull"`
527+
OrganizationField serpent.String `json:"organization_field" typescript:",notnull"`
528+
OrganizationMapping serpent.Struct[map[string][]uuid.UUID] `json:"organization_mapping" typescript:",notnull"`
529+
OrganizationAssignDefault serpent.Bool `json:"organization_assign_default" typescript:",notnull"`
530+
GroupAutoCreate serpent.Bool `json:"group_auto_create" typescript:",notnull"`
531+
GroupRegexFilter serpent.Regexp `json:"group_regex_filter" typescript:",notnull"`
532+
GroupAllowList serpent.StringArray `json:"group_allow_list" typescript:",notnull"`
533+
GroupField serpent.String `json:"groups_field" typescript:",notnull"`
534+
GroupMapping serpent.Struct[map[string]string] `json:"group_mapping" typescript:",notnull"`
535+
UserRoleField serpent.String `json:"user_role_field" typescript:",notnull"`
536+
UserRoleMapping serpent.Struct[map[string][]string] `json:"user_role_mapping" typescript:",notnull"`
537+
UserRolesDefault serpent.StringArray `json:"user_roles_default" typescript:",notnull"`
538+
SignInText serpent.String `json:"sign_in_text" typescript:",notnull"`
539+
IconURL serpent.URL `json:"icon_url" typescript:",notnull"`
540+
SignupsDisabledText serpent.String `json:"signups_disabled_text" typescript:",notnull"`
541+
SkipIssuerChecks serpent.Bool `json:"skip_issuer_checks" typescript:",notnull"`
538542
}
539543

540544
type TelemetryConfig struct {

0 commit comments

Comments
 (0)