Skip to content

Commit fccd2b8

Browse files
committed
chore: refactor into agpl and enterprise
1 parent b9abffe commit fccd2b8

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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy