mirror of
https://github.com/coder/coder.git
synced 2025-07-13 21:36:50 +00:00
feat: accept provisioner keys for provisioner auth (#13972)
This commit is contained in:
@ -245,6 +245,7 @@ var (
|
||||
rbac.ResourceOrganization.Type: {policy.ActionCreate, policy.ActionRead},
|
||||
rbac.ResourceOrganizationMember.Type: {policy.ActionCreate},
|
||||
rbac.ResourceProvisionerDaemon.Type: {policy.ActionCreate, policy.ActionUpdate},
|
||||
rbac.ResourceProvisionerKeys.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionDelete},
|
||||
rbac.ResourceUser.Type: rbac.ResourceUser.AvailableActions(),
|
||||
rbac.ResourceWorkspaceDormant.Type: {policy.ActionUpdate, policy.ActionDelete, policy.ActionWorkspaceStop},
|
||||
rbac.ResourceWorkspace.Type: {policy.ActionUpdate, policy.ActionDelete, policy.ActionWorkspaceStart, policy.ActionWorkspaceStop, policy.ActionSSH},
|
||||
|
@ -93,6 +93,13 @@ func CSRF(secureCookie bool) func(next http.Handler) http.Handler {
|
||||
return true
|
||||
}
|
||||
|
||||
if r.Header.Get(codersdk.ProvisionerDaemonKey) != "" {
|
||||
// If present, the provisioner daemon also is providing an api key
|
||||
// that will make them exempt from CSRF. But this is still useful
|
||||
// for enumerating the external auths.
|
||||
return true
|
||||
}
|
||||
|
||||
// If the X-CSRF-TOKEN header is set, we can exempt the func if it's valid.
|
||||
// This is the CSRF check.
|
||||
sent := r.Header.Get("X-CSRF-TOKEN")
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
"github.com/coder/coder/v2/coderd/httpapi"
|
||||
"github.com/coder/coder/v2/coderd/provisionerkey"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
@ -19,11 +20,13 @@ func ProvisionerDaemonAuthenticated(r *http.Request) bool {
|
||||
}
|
||||
|
||||
type ExtractProvisionerAuthConfig struct {
|
||||
DB database.Store
|
||||
Optional bool
|
||||
DB database.Store
|
||||
Optional bool
|
||||
PSK string
|
||||
MultiOrgEnabled bool
|
||||
}
|
||||
|
||||
func ExtractProvisionerDaemonAuthenticated(opts ExtractProvisionerAuthConfig, psk string) func(next http.Handler) http.Handler {
|
||||
func ExtractProvisionerDaemonAuthenticated(opts ExtractProvisionerAuthConfig) func(next http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
@ -36,37 +39,103 @@ func ExtractProvisionerDaemonAuthenticated(opts ExtractProvisionerAuthConfig, ps
|
||||
httpapi.Write(ctx, w, code, response)
|
||||
}
|
||||
|
||||
if psk == "" {
|
||||
// No psk means external provisioner daemons are not allowed.
|
||||
// So their auth is not valid.
|
||||
if !opts.MultiOrgEnabled {
|
||||
if opts.PSK == "" {
|
||||
handleOptional(http.StatusUnauthorized, codersdk.Response{
|
||||
Message: "External provisioner daemons not enabled",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
fallbackToPSK(ctx, opts.PSK, next, w, r, handleOptional)
|
||||
return
|
||||
}
|
||||
|
||||
psk := r.Header.Get(codersdk.ProvisionerDaemonPSK)
|
||||
key := r.Header.Get(codersdk.ProvisionerDaemonKey)
|
||||
if key == "" {
|
||||
if opts.PSK == "" {
|
||||
handleOptional(http.StatusUnauthorized, codersdk.Response{
|
||||
Message: "provisioner daemon key required",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
fallbackToPSK(ctx, opts.PSK, next, w, r, handleOptional)
|
||||
return
|
||||
}
|
||||
if psk != "" {
|
||||
handleOptional(http.StatusBadRequest, codersdk.Response{
|
||||
Message: "External provisioner daemons not enabled",
|
||||
Message: "provisioner daemon key and psk provided, but only one is allowed",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
token := r.Header.Get(codersdk.ProvisionerDaemonPSK)
|
||||
if token == "" {
|
||||
id, keyValue, err := provisionerkey.Parse(key)
|
||||
if err != nil {
|
||||
handleOptional(http.StatusUnauthorized, codersdk.Response{
|
||||
Message: "provisioner daemon auth token required",
|
||||
Message: "provisioner daemon key invalid",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if subtle.ConstantTimeCompare([]byte(token), []byte(psk)) != 1 {
|
||||
// nolint:gocritic // System must check if the provisioner key is valid.
|
||||
pk, err := opts.DB.GetProvisionerKeyByID(dbauthz.AsSystemRestricted(ctx), id)
|
||||
if err != nil {
|
||||
if httpapi.Is404Error(err) {
|
||||
handleOptional(http.StatusUnauthorized, codersdk.Response{
|
||||
Message: "provisioner daemon key invalid",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
handleOptional(http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "get provisioner daemon key: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if provisionerkey.Compare(pk.HashedSecret, provisionerkey.HashSecret(keyValue)) {
|
||||
handleOptional(http.StatusUnauthorized, codersdk.Response{
|
||||
Message: "provisioner daemon auth token invalid",
|
||||
Message: "provisioner daemon key invalid",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// The PSK does not indicate a specific provisioner daemon. So just
|
||||
// The provisioner key does not indicate a specific provisioner daemon. So just
|
||||
// store a boolean so the caller can check if the request is from an
|
||||
// authenticated provisioner daemon.
|
||||
ctx = context.WithValue(ctx, provisionerDaemonContextKey{}, true)
|
||||
// store key used to authenticate the request
|
||||
ctx = context.WithValue(ctx, provisionerKeyAuthContextKey{}, pk)
|
||||
// nolint:gocritic // Authenticating as a provisioner daemon.
|
||||
ctx = dbauthz.AsProvisionerd(ctx)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type provisionerKeyAuthContextKey struct{}
|
||||
|
||||
func ProvisionerKeyAuthOptional(r *http.Request) (database.ProvisionerKey, bool) {
|
||||
user, ok := r.Context().Value(provisionerKeyAuthContextKey{}).(database.ProvisionerKey)
|
||||
return user, ok
|
||||
}
|
||||
|
||||
func fallbackToPSK(ctx context.Context, psk string, next http.Handler, w http.ResponseWriter, r *http.Request, handleOptional func(code int, response codersdk.Response)) {
|
||||
token := r.Header.Get(codersdk.ProvisionerDaemonPSK)
|
||||
if subtle.ConstantTimeCompare([]byte(token), []byte(psk)) != 1 {
|
||||
handleOptional(http.StatusUnauthorized, codersdk.Response{
|
||||
Message: "provisioner daemon psk invalid",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// The PSK does not indicate a specific provisioner daemon. So just
|
||||
// store a boolean so the caller can check if the request is from an
|
||||
// authenticated provisioner daemon.
|
||||
ctx = context.WithValue(ctx, provisionerDaemonContextKey{}, true)
|
||||
// nolint:gocritic // Authenticating as a provisioner daemon.
|
||||
ctx = dbauthz.AsProvisionerd(ctx)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
}
|
||||
|
@ -2,7 +2,9 @@ package provisionerkey
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/xerrors"
|
||||
@ -18,7 +20,7 @@ func New(organizationID uuid.UUID, name string) (database.InsertProvisionerKeyPa
|
||||
if err != nil {
|
||||
return database.InsertProvisionerKeyParams{}, "", xerrors.Errorf("generate token: %w", err)
|
||||
}
|
||||
hashedSecret := sha256.Sum256([]byte(secret))
|
||||
hashedSecret := HashSecret(secret)
|
||||
token := fmt.Sprintf("%s:%s", id, secret)
|
||||
|
||||
return database.InsertProvisionerKeyParams{
|
||||
@ -26,6 +28,29 @@ func New(organizationID uuid.UUID, name string) (database.InsertProvisionerKeyPa
|
||||
CreatedAt: dbtime.Now(),
|
||||
OrganizationID: organizationID,
|
||||
Name: name,
|
||||
HashedSecret: hashedSecret[:],
|
||||
HashedSecret: hashedSecret,
|
||||
}, token, nil
|
||||
}
|
||||
|
||||
func Parse(token string) (uuid.UUID, string, error) {
|
||||
parts := strings.Split(token, ":")
|
||||
if len(parts) != 2 {
|
||||
return uuid.UUID{}, "", xerrors.Errorf("invalid token format")
|
||||
}
|
||||
|
||||
id, err := uuid.Parse(parts[0])
|
||||
if err != nil {
|
||||
return uuid.UUID{}, "", xerrors.Errorf("parse id: %w", err)
|
||||
}
|
||||
|
||||
return id, parts[1], nil
|
||||
}
|
||||
|
||||
func HashSecret(secret string) []byte {
|
||||
h := sha256.Sum256([]byte(secret))
|
||||
return h[:]
|
||||
}
|
||||
|
||||
func Compare(a []byte, b []byte) bool {
|
||||
return subtle.ConstantTimeCompare(a, b) != 1
|
||||
}
|
||||
|
Reference in New Issue
Block a user