Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cli/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import (
"testing"
"time"

"github.com/go-chi/chi"
"github.com/go-chi/chi/v5"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/goleak"
Expand Down
6 changes: 1 addition & 5 deletions coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,11 +222,7 @@ func New(options *Options) *API {
r.Route("/api/v2", func(r chi.Router) {
api.APIHandler = r

r.NotFound(func(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(rw, http.StatusNotFound, codersdk.Response{
Message: "Route not found.",
})
})
r.NotFound(func(rw http.ResponseWriter, r *http.Request) { httpapi.RouteNotFound(rw) })
r.Use(
tracing.Middleware(api.TracerProvider),
// Specific routes can specify smaller limits.
Expand Down
6 changes: 6 additions & 0 deletions coderd/httpapi/httpapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ func InternalServerError(rw http.ResponseWriter, err error) {
})
}

func RouteNotFound(rw http.ResponseWriter) {
Write(rw, http.StatusNotFound, codersdk.Response{
Message: "Route not found.",
})
}

// Write outputs a standardized format to an HTTP response body.
func Write(rw http.ResponseWriter, status int, response interface{}) {
buf := &bytes.Buffer{}
Expand Down
2 changes: 1 addition & 1 deletion coderd/userauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ func (api *API) oauthLogin(r *http.Request, params oauthLoginParams) (*http.Cook
organizationID = organizations[0].ID
}

user, _, err = api.createUser(ctx, tx, createUserRequest{
user, _, err = api.CreateUser(ctx, tx, CreateUserRequest{
CreateUserRequest: codersdk.CreateUserRequest{
Email: params.Email,
Username: params.Username,
Expand Down
8 changes: 4 additions & 4 deletions coderd/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ func (api *API) postFirstUser(rw http.ResponseWriter, r *http.Request) {
return
}

user, organizationID, err := api.createUser(r.Context(), api.Database, createUserRequest{
user, organizationID, err := api.CreateUser(r.Context(), api.Database, CreateUserRequest{
CreateUserRequest: codersdk.CreateUserRequest{
Email: createUser.Email,
Username: createUser.Username,
Expand Down Expand Up @@ -317,7 +317,7 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) {
return
}

user, _, err := api.createUser(r.Context(), api.Database, createUserRequest{
user, _, err := api.CreateUser(r.Context(), api.Database, CreateUserRequest{
CreateUserRequest: req,
LoginType: database.LoginTypePassword,
})
Expand Down Expand Up @@ -1101,12 +1101,12 @@ func (api *API) createAPIKey(r *http.Request, params createAPIKeyParams) (*http.
}, nil
}

type createUserRequest struct {
type CreateUserRequest struct {
codersdk.CreateUserRequest
LoginType database.LoginType
}

func (api *API) createUser(ctx context.Context, store database.Store, req createUserRequest) (database.User, uuid.UUID, error) {
func (api *API) CreateUser(ctx context.Context, store database.Store, req CreateUserRequest) (database.User, uuid.UUID, error) {
var user database.User
return user, req.OrganizationID, store.InTx(func(tx database.Store) error {
orgRoles := make([]string, 0)
Expand Down
3 changes: 2 additions & 1 deletion codersdk/features.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ const (
const (
FeatureUserLimit = "user_limit"
FeatureAuditLog = "audit_log"
FeatureSCIM = "scim"
)

var FeatureNames = []string{FeatureUserLimit, FeatureAuditLog}
var FeatureNames = []string{FeatureUserLimit, FeatureAuditLog, FeatureSCIM}

type Feature struct {
Entitlement Entitlement `json:"entitlement"`
Expand Down
11 changes: 11 additions & 0 deletions docs/admin/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,14 @@ CODER_OIDC_CLIENT_SECRET="G0CSP...7qSM"
Once complete, run `sudo service coder restart` to reboot Coder.

> 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. `example@coder.com` becomes `example`).

## SCIM

Coder supports user provisioning and deprovisioning via SCIM 2.0 with header
authentication. Upon deactivation, users are [suspended](userd.md#suspend-a-user)
and are not deleted. [Configure](./configure.md) your SCIM application with an
auth key and supply it the Coder server.

```console
CODER_SCIM_API_KEY="your-api-key"
```
5 changes: 4 additions & 1 deletion enterprise/cli/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ import (

func server() *cobra.Command {
var (
auditLogging bool
auditLogging bool
scimAuthHeader string
)
cmd := agpl.Server(func(ctx context.Context, options *agplcoderd.Options) (*agplcoderd.API, error) {
api, err := coderd.New(ctx, &coderd.Options{
AuditLogging: auditLogging,
SCIMAPIKey: []byte(scimAuthHeader),
Options: options,
})
if err != nil {
Expand All @@ -28,6 +30,7 @@ func server() *cobra.Command {
})
cliflag.BoolVarP(cmd.Flags(), &auditLogging, "audit-logging", "", "CODER_AUDIT_LOGGING", true,
"Specifies whether audit logging is enabled.")
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.")

return cmd
}
19 changes: 19 additions & 0 deletions enterprise/coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,19 @@ func New(ctx context.Context, options *Options) (*API, error) {
})
})

if len(options.SCIMAPIKey) != 0 {
api.AGPL.RootHandler.Route("/scim/v2", func(r chi.Router) {
r.Use(api.scimEnabledMW)
r.Post("/Users", api.scimPostUser)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Capital U? :o

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this is the SCIM spec :(

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

craaaazy

r.Route("/Users", func(r chi.Router) {
r.Get("/", api.scimGetUsers)
r.Post("/", api.scimPostUser)
r.Get("/{id}", api.scimGetUser)
r.Patch("/{id}", api.scimPatchUser)
})
})
}

err := api.updateEntitlements(ctx)
if err != nil {
return nil, xerrors.Errorf("update entitlements: %w", err)
Expand All @@ -76,6 +89,7 @@ type Options struct {
*coderd.Options

AuditLogging bool
SCIMAPIKey []byte
EntitlementsUpdateInterval time.Duration
Keys map[string]ed25519.PublicKey
}
Expand All @@ -93,6 +107,7 @@ type entitlements struct {
hasLicense bool
activeUsers codersdk.Feature
auditLogs codersdk.Entitlement
scim codersdk.Entitlement
}

func (api *API) Close() error {
Expand All @@ -117,6 +132,7 @@ func (api *API) updateEntitlements(ctx context.Context) error {
Entitlement: codersdk.EntitlementNotEntitled,
},
auditLogs: codersdk.EntitlementNotEntitled,
scim: codersdk.EntitlementNotEntitled,
}

// Here we loop through licenses to detect enabled features.
Expand Down Expand Up @@ -149,6 +165,9 @@ func (api *API) updateEntitlements(ctx context.Context) error {
if claims.Features.AuditLog > 0 {
entitlements.auditLogs = entitlement
}
if claims.Features.SCIM > 0 {
entitlements.scim = entitlement
}
}

if entitlements.auditLogs != api.entitlements.auditLogs {
Expand Down
9 changes: 9 additions & 0 deletions enterprise/coderd/coderdenttest/coderdenttest.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ func init() {
type Options struct {
*coderdtest.Options
EntitlementsUpdateInterval time.Duration
SCIMAPIKey []byte
}

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

// AddLicense generates a new license with the options provided and inserts it.
Expand All @@ -105,6 +108,11 @@ func GenerateLicense(t *testing.T, options LicenseOptions) string {
if options.AuditLog {
auditLog = 1
}
scim := int64(0)
if options.SCIM {
scim = 1
}

c := &coderd.Claims{
RegisteredClaims: jwt.RegisteredClaims{
Issuer: "test@testing.test",
Expand All @@ -119,6 +127,7 @@ func GenerateLicense(t *testing.T, options LicenseOptions) string {
Features: coderd.Features{
UserLimit: options.UserLimit,
AuditLog: auditLog,
SCIM: scim,
},
}
tok := jwt.NewWithClaims(jwt.SigningMethodEdDSA, c)
Expand Down
1 change: 1 addition & 0 deletions enterprise/coderd/licenses.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ var Keys = map[string]ed25519.PublicKey{"2022-08-12": ed25519.PublicKey(key20220
type Features struct {
UserLimit int64 `json:"user_limit"`
AuditLog int64 `json:"audit_log"`
SCIM int64 `json:"scim"`
}

type Claims struct {
Expand Down
4 changes: 4 additions & 0 deletions enterprise/coderd/licenses_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,13 @@ func TestGetLicense(t *testing.T) {
coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
AccountID: "testing",
AuditLog: true,
SCIM: true,
})

coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
AccountID: "testing2",
AuditLog: true,
SCIM: true,
UserLimit: 200,
})

Expand All @@ -96,12 +98,14 @@ func TestGetLicense(t *testing.T) {
assert.Equal(t, map[string]interface{}{
codersdk.FeatureUserLimit: json.Number("0"),
codersdk.FeatureAuditLog: json.Number("1"),
codersdk.FeatureSCIM: json.Number("1"),
}, licenses[0].Claims["features"])
assert.Equal(t, int32(2), licenses[1].ID)
assert.Equal(t, "testing2", licenses[1].Claims["account_id"])
assert.Equal(t, map[string]interface{}{
codersdk.FeatureUserLimit: json.Number("200"),
codersdk.FeatureAuditLog: json.Number("1"),
codersdk.FeatureSCIM: json.Number("1"),
}, licenses[1].Claims["features"])
})
}
Expand Down
Loading