Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 5e2efb6

Browse files
coadlerbpmct
andauthored
feat: add SCIM provisioning via Okta (#4132)
Co-authored-by: Ben Potter <[email protected]>
1 parent 50321ba commit 5e2efb6

File tree

16 files changed

+467
-13
lines changed

16 files changed

+467
-13
lines changed

cli/server_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import (
2424
"testing"
2525
"time"
2626

27-
"github.com/go-chi/chi"
27+
"github.com/go-chi/chi/v5"
2828
"github.com/stretchr/testify/assert"
2929
"github.com/stretchr/testify/require"
3030
"go.uber.org/goleak"

coderd/coderd.go

+1-5
Original file line numberDiff line numberDiff line change
@@ -222,11 +222,7 @@ func New(options *Options) *API {
222222
r.Route("/api/v2", func(r chi.Router) {
223223
api.APIHandler = r
224224

225-
r.NotFound(func(rw http.ResponseWriter, r *http.Request) {
226-
httpapi.Write(rw, http.StatusNotFound, codersdk.Response{
227-
Message: "Route not found.",
228-
})
229-
})
225+
r.NotFound(func(rw http.ResponseWriter, r *http.Request) { httpapi.RouteNotFound(rw) })
230226
r.Use(
231227
tracing.Middleware(api.TracerProvider),
232228
// Specific routes can specify smaller limits.

coderd/httpapi/httpapi.go

+6
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,12 @@ func InternalServerError(rw http.ResponseWriter, err error) {
7575
})
7676
}
7777

78+
func RouteNotFound(rw http.ResponseWriter) {
79+
Write(rw, http.StatusNotFound, codersdk.Response{
80+
Message: "Route not found.",
81+
})
82+
}
83+
7884
// Write outputs a standardized format to an HTTP response body.
7985
func Write(rw http.ResponseWriter, status int, response interface{}) {
8086
buf := &bytes.Buffer{}

coderd/userauth.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,7 @@ func (api *API) oauthLogin(r *http.Request, params oauthLoginParams) (*http.Cook
378378
organizationID = organizations[0].ID
379379
}
380380

381-
user, _, err = api.createUser(ctx, tx, createUserRequest{
381+
user, _, err = api.CreateUser(ctx, tx, CreateUserRequest{
382382
CreateUserRequest: codersdk.CreateUserRequest{
383383
Email: params.Email,
384384
Username: params.Username,

coderd/users.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ func (api *API) postFirstUser(rw http.ResponseWriter, r *http.Request) {
8383
return
8484
}
8585

86-
user, organizationID, err := api.createUser(r.Context(), api.Database, createUserRequest{
86+
user, organizationID, err := api.CreateUser(r.Context(), api.Database, CreateUserRequest{
8787
CreateUserRequest: codersdk.CreateUserRequest{
8888
Email: createUser.Email,
8989
Username: createUser.Username,
@@ -317,7 +317,7 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) {
317317
return
318318
}
319319

320-
user, _, err := api.createUser(r.Context(), api.Database, createUserRequest{
320+
user, _, err := api.CreateUser(r.Context(), api.Database, CreateUserRequest{
321321
CreateUserRequest: req,
322322
LoginType: database.LoginTypePassword,
323323
})
@@ -1101,12 +1101,12 @@ func (api *API) createAPIKey(r *http.Request, params createAPIKeyParams) (*http.
11011101
}, nil
11021102
}
11031103

1104-
type createUserRequest struct {
1104+
type CreateUserRequest struct {
11051105
codersdk.CreateUserRequest
11061106
LoginType database.LoginType
11071107
}
11081108

1109-
func (api *API) createUser(ctx context.Context, store database.Store, req createUserRequest) (database.User, uuid.UUID, error) {
1109+
func (api *API) CreateUser(ctx context.Context, store database.Store, req CreateUserRequest) (database.User, uuid.UUID, error) {
11101110
var user database.User
11111111
return user, req.OrganizationID, store.InTx(func(tx database.Store) error {
11121112
orgRoles := make([]string, 0)

codersdk/features.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ const (
1717
const (
1818
FeatureUserLimit = "user_limit"
1919
FeatureAuditLog = "audit_log"
20+
FeatureSCIM = "scim"
2021
)
2122

22-
var FeatureNames = []string{FeatureUserLimit, FeatureAuditLog}
23+
var FeatureNames = []string{FeatureUserLimit, FeatureAuditLog, FeatureSCIM}
2324

2425
type Feature struct {
2526
Entitlement Entitlement `json:"entitlement"`

docs/admin/auth.md

+11
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,14 @@ CODER_OIDC_CLIENT_SECRET="G0CSP...7qSM"
7474
Once complete, run `sudo service coder restart` to reboot Coder.
7575

7676
> When a new user is created, the `preferred_username` claim becomes the username. If this claim is empty, the email address will be stripped of the domain, and become the username (e.g. `[email protected]` becomes `example`).
77+
78+
## SCIM
79+
80+
Coder supports user provisioning and deprovisioning via SCIM 2.0 with header
81+
authentication. Upon deactivation, users are [suspended](userd.md#suspend-a-user)
82+
and are not deleted. [Configure](./configure.md) your SCIM application with an
83+
auth key and supply it the Coder server.
84+
85+
```console
86+
CODER_SCIM_API_KEY="your-api-key"
87+
```

enterprise/cli/server.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@ import (
1414

1515
func server() *cobra.Command {
1616
var (
17-
auditLogging bool
17+
auditLogging bool
18+
scimAuthHeader string
1819
)
1920
cmd := agpl.Server(func(ctx context.Context, options *agplcoderd.Options) (*agplcoderd.API, error) {
2021
api, err := coderd.New(ctx, &coderd.Options{
2122
AuditLogging: auditLogging,
23+
SCIMAPIKey: []byte(scimAuthHeader),
2224
Options: options,
2325
})
2426
if err != nil {
@@ -28,6 +30,7 @@ func server() *cobra.Command {
2830
})
2931
cliflag.BoolVarP(cmd.Flags(), &auditLogging, "audit-logging", "", "CODER_AUDIT_LOGGING", true,
3032
"Specifies whether audit logging is enabled.")
33+
cliflag.StringVarP(cmd.Flags(), &scimAuthHeader, "scim-auth-header", "", "CODER_SCIM_API_KEY", "", "Enables SCIM and sets the authentication header for the built-in SCIM server. New users are automatically created with OIDC authentication.")
3134

3235
return cmd
3336
}

enterprise/coderd/coderd.go

+19
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,19 @@ func New(ctx context.Context, options *Options) (*API, error) {
6363
})
6464
})
6565

66+
if len(options.SCIMAPIKey) != 0 {
67+
api.AGPL.RootHandler.Route("/scim/v2", func(r chi.Router) {
68+
r.Use(api.scimEnabledMW)
69+
r.Post("/Users", api.scimPostUser)
70+
r.Route("/Users", func(r chi.Router) {
71+
r.Get("/", api.scimGetUsers)
72+
r.Post("/", api.scimPostUser)
73+
r.Get("/{id}", api.scimGetUser)
74+
r.Patch("/{id}", api.scimPatchUser)
75+
})
76+
})
77+
}
78+
6679
err := api.updateEntitlements(ctx)
6780
if err != nil {
6881
return nil, xerrors.Errorf("update entitlements: %w", err)
@@ -76,6 +89,7 @@ type Options struct {
7689
*coderd.Options
7790

7891
AuditLogging bool
92+
SCIMAPIKey []byte
7993
EntitlementsUpdateInterval time.Duration
8094
Keys map[string]ed25519.PublicKey
8195
}
@@ -93,6 +107,7 @@ type entitlements struct {
93107
hasLicense bool
94108
activeUsers codersdk.Feature
95109
auditLogs codersdk.Entitlement
110+
scim codersdk.Entitlement
96111
}
97112

98113
func (api *API) Close() error {
@@ -117,6 +132,7 @@ func (api *API) updateEntitlements(ctx context.Context) error {
117132
Entitlement: codersdk.EntitlementNotEntitled,
118133
},
119134
auditLogs: codersdk.EntitlementNotEntitled,
135+
scim: codersdk.EntitlementNotEntitled,
120136
}
121137

122138
// Here we loop through licenses to detect enabled features.
@@ -149,6 +165,9 @@ func (api *API) updateEntitlements(ctx context.Context) error {
149165
if claims.Features.AuditLog > 0 {
150166
entitlements.auditLogs = entitlement
151167
}
168+
if claims.Features.SCIM > 0 {
169+
entitlements.scim = entitlement
170+
}
152171
}
153172

154173
if entitlements.auditLogs != api.entitlements.auditLogs {

enterprise/coderd/coderdenttest/coderdenttest.go

+9
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ func init() {
3737
type Options struct {
3838
*coderdtest.Options
3939
EntitlementsUpdateInterval time.Duration
40+
SCIMAPIKey []byte
4041
}
4142

4243
// New constructs a codersdk client connected to an in-memory Enterprise API instance.
@@ -55,6 +56,7 @@ func NewWithAPI(t *testing.T, options *Options) (*codersdk.Client, io.Closer, *c
5556
srv, cancelFunc, oop := coderdtest.NewOptions(t, options.Options)
5657
coderAPI, err := coderd.New(context.Background(), &coderd.Options{
5758
AuditLogging: true,
59+
SCIMAPIKey: options.SCIMAPIKey,
5860
Options: oop,
5961
EntitlementsUpdateInterval: options.EntitlementsUpdateInterval,
6062
Keys: map[string]ed25519.PublicKey{
@@ -82,6 +84,7 @@ type LicenseOptions struct {
8284
ExpiresAt time.Time
8385
UserLimit int64
8486
AuditLog bool
87+
SCIM bool
8588
}
8689

8790
// AddLicense generates a new license with the options provided and inserts it.
@@ -105,6 +108,11 @@ func GenerateLicense(t *testing.T, options LicenseOptions) string {
105108
if options.AuditLog {
106109
auditLog = 1
107110
}
111+
scim := int64(0)
112+
if options.SCIM {
113+
scim = 1
114+
}
115+
108116
c := &coderd.Claims{
109117
RegisteredClaims: jwt.RegisteredClaims{
110118
Issuer: "[email protected]",
@@ -119,6 +127,7 @@ func GenerateLicense(t *testing.T, options LicenseOptions) string {
119127
Features: coderd.Features{
120128
UserLimit: options.UserLimit,
121129
AuditLog: auditLog,
130+
SCIM: scim,
122131
},
123132
}
124133
tok := jwt.NewWithClaims(jwt.SigningMethodEdDSA, c)

enterprise/coderd/licenses.go

+1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ var Keys = map[string]ed25519.PublicKey{"2022-08-12": ed25519.PublicKey(key20220
4747
type Features struct {
4848
UserLimit int64 `json:"user_limit"`
4949
AuditLog int64 `json:"audit_log"`
50+
SCIM int64 `json:"scim"`
5051
}
5152

5253
type Claims struct {

enterprise/coderd/licenses_test.go

+4
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,13 @@ func TestGetLicense(t *testing.T) {
8080
coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
8181
AccountID: "testing",
8282
AuditLog: true,
83+
SCIM: true,
8384
})
8485

8586
coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
8687
AccountID: "testing2",
8788
AuditLog: true,
89+
SCIM: true,
8890
UserLimit: 200,
8991
})
9092

@@ -96,12 +98,14 @@ func TestGetLicense(t *testing.T) {
9698
assert.Equal(t, map[string]interface{}{
9799
codersdk.FeatureUserLimit: json.Number("0"),
98100
codersdk.FeatureAuditLog: json.Number("1"),
101+
codersdk.FeatureSCIM: json.Number("1"),
99102
}, licenses[0].Claims["features"])
100103
assert.Equal(t, int32(2), licenses[1].ID)
101104
assert.Equal(t, "testing2", licenses[1].Claims["account_id"])
102105
assert.Equal(t, map[string]interface{}{
103106
codersdk.FeatureUserLimit: json.Number("200"),
104107
codersdk.FeatureAuditLog: json.Number("1"),
108+
codersdk.FeatureSCIM: json.Number("1"),
105109
}, licenses[1].Claims["features"])
106110
})
107111
}

0 commit comments

Comments
 (0)