mirror of https://github.com/coder/coder.git
chore: scope provisioner daemons to organizations
This commit is contained in:
parent
7fab067307
commit
184193ef41
|
@ -1239,10 +1239,17 @@ func (api *API) CreateInMemoryProvisionerDaemon(dialCtx context.Context, name st
|
|||
}
|
||||
}()
|
||||
|
||||
// All in memory provisioners will be apart of the default org for now.
|
||||
defaultOrg, err := api.Database.GetDefaultOrganization(dbauthz.AsSystemRestricted(dialCtx))
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("unable to fetch default org for in memory provisioner: %w", err)
|
||||
}
|
||||
|
||||
//nolint:gocritic // in-memory provisioners are owned by system
|
||||
daemon, err := api.Database.UpsertProvisionerDaemon(dbauthz.AsSystemRestricted(dialCtx), database.UpsertProvisionerDaemonParams{
|
||||
Name: name,
|
||||
CreatedAt: dbtime.Now(),
|
||||
Name: name,
|
||||
OrganizationID: defaultOrg.ID,
|
||||
CreatedAt: dbtime.Now(),
|
||||
Provisioners: []database.ProvisionerType{
|
||||
database.ProvisionerTypeEcho, database.ProvisionerTypeTerraform,
|
||||
},
|
||||
|
|
|
@ -162,6 +162,9 @@ var (
|
|||
DisplayName: "Provisioner Daemon",
|
||||
Site: rbac.Permissions(map[string][]rbac.Action{
|
||||
// TODO: Add ProvisionerJob resource type.
|
||||
// Might want to reduce the org read scope to a specific org
|
||||
// in the future.
|
||||
rbac.ResourceOrganization.Type: {rbac.ActionRead},
|
||||
rbac.ResourceFile.Type: {rbac.ActionRead},
|
||||
rbac.ResourceSystem.Type: {rbac.WildcardSymbol},
|
||||
rbac.ResourceTemplate.Type: {rbac.ActionRead, rbac.ActionUpdate},
|
||||
|
|
|
@ -609,7 +609,8 @@ CREATE TABLE provisioner_daemons (
|
|||
tags jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
last_seen_at timestamp with time zone,
|
||||
version text DEFAULT ''::text NOT NULL,
|
||||
api_version text DEFAULT '1.0'::text NOT NULL
|
||||
api_version text DEFAULT '1.0'::text NOT NULL,
|
||||
organization_id uuid NOT NULL
|
||||
);
|
||||
|
||||
COMMENT ON COLUMN provisioner_daemons.api_version IS 'The API version of the provisioner daemon';
|
||||
|
@ -1682,6 +1683,9 @@ ALTER TABLE ONLY organization_members
|
|||
ALTER TABLE ONLY parameter_schemas
|
||||
ADD CONSTRAINT parameter_schemas_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY provisioner_daemons
|
||||
ADD CONSTRAINT provisioner_daemons_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY provisioner_job_logs
|
||||
ADD CONSTRAINT provisioner_job_logs_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE;
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ const (
|
|||
ForeignKeyOrganizationMembersOrganizationIDUUID ForeignKeyConstraint = "organization_members_organization_id_uuid_fkey" // ALTER TABLE ONLY organization_members ADD CONSTRAINT organization_members_organization_id_uuid_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE;
|
||||
ForeignKeyOrganizationMembersUserIDUUID ForeignKeyConstraint = "organization_members_user_id_uuid_fkey" // ALTER TABLE ONLY organization_members ADD CONSTRAINT organization_members_user_id_uuid_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
ForeignKeyParameterSchemasJobID ForeignKeyConstraint = "parameter_schemas_job_id_fkey" // ALTER TABLE ONLY parameter_schemas ADD CONSTRAINT parameter_schemas_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE;
|
||||
ForeignKeyProvisionerDaemonsOrganizationID ForeignKeyConstraint = "provisioner_daemons_organization_id_fkey" // ALTER TABLE ONLY provisioner_daemons ADD CONSTRAINT provisioner_daemons_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE;
|
||||
ForeignKeyProvisionerJobLogsJobID ForeignKeyConstraint = "provisioner_job_logs_job_id_fkey" // ALTER TABLE ONLY provisioner_job_logs ADD CONSTRAINT provisioner_job_logs_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE;
|
||||
ForeignKeyProvisionerJobsOrganizationID ForeignKeyConstraint = "provisioner_jobs_organization_id_fkey" // ALTER TABLE ONLY provisioner_jobs ADD CONSTRAINT provisioner_jobs_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE;
|
||||
ForeignKeyTailnetAgentsCoordinatorID ForeignKeyConstraint = "tailnet_agents_coordinator_id_fkey" // ALTER TABLE ONLY tailnet_agents ADD CONSTRAINT tailnet_agents_coordinator_id_fkey FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators(id) ON DELETE CASCADE;
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE provisioner_daemons
|
||||
DROP COLUMN organization_id;
|
|
@ -0,0 +1,14 @@
|
|||
-- At the time of this migration, only 1 org is expected in a deployment.
|
||||
-- In the future when multi-org is more common, there might be a use case
|
||||
-- to allow a provisioner to be associated with multiple orgs.
|
||||
ALTER TABLE provisioner_daemons
|
||||
ADD COLUMN organization_id UUID REFERENCES organizations(id) ON DELETE CASCADE;
|
||||
|
||||
UPDATE
|
||||
provisioner_daemons
|
||||
SET
|
||||
-- Default to the first org
|
||||
organization_id = (SELECT id FROM organizations ORDER BY organizations.created_at ASC LIMIT 1 );
|
||||
|
||||
ALTER TABLE provisioner_daemons
|
||||
ALTER COLUMN organization_id SET NOT NULL;
|
|
@ -1908,7 +1908,8 @@ type ProvisionerDaemon struct {
|
|||
LastSeenAt sql.NullTime `db:"last_seen_at" json:"last_seen_at"`
|
||||
Version string `db:"version" json:"version"`
|
||||
// The API version of the provisioner daemon
|
||||
APIVersion string `db:"api_version" json:"api_version"`
|
||||
APIVersion string `db:"api_version" json:"api_version"`
|
||||
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
|
||||
}
|
||||
|
||||
type ProvisionerJob struct {
|
||||
|
|
|
@ -3686,7 +3686,7 @@ func (q *sqlQuerier) DeleteOldProvisionerDaemons(ctx context.Context) error {
|
|||
|
||||
const getProvisionerDaemons = `-- name: GetProvisionerDaemons :many
|
||||
SELECT
|
||||
id, created_at, name, provisioners, replica_id, tags, last_seen_at, version, api_version
|
||||
id, created_at, name, provisioners, replica_id, tags, last_seen_at, version, api_version, organization_id
|
||||
FROM
|
||||
provisioner_daemons
|
||||
`
|
||||
|
@ -3710,6 +3710,7 @@ func (q *sqlQuerier) GetProvisionerDaemons(ctx context.Context) ([]ProvisionerDa
|
|||
&i.LastSeenAt,
|
||||
&i.Version,
|
||||
&i.APIVersion,
|
||||
&i.OrganizationID,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -3754,6 +3755,7 @@ INSERT INTO
|
|||
tags,
|
||||
last_seen_at,
|
||||
"version",
|
||||
organization_id,
|
||||
api_version
|
||||
)
|
||||
VALUES (
|
||||
|
@ -3764,27 +3766,29 @@ VALUES (
|
|||
$4,
|
||||
$5,
|
||||
$6,
|
||||
$7
|
||||
$7,
|
||||
$8
|
||||
) ON CONFLICT("name", LOWER(COALESCE(tags ->> 'owner'::text, ''::text))) DO UPDATE SET
|
||||
provisioners = $3,
|
||||
tags = $4,
|
||||
last_seen_at = $5,
|
||||
"version" = $6,
|
||||
api_version = $7
|
||||
api_version = $8
|
||||
WHERE
|
||||
-- Only ones with the same tags are allowed clobber
|
||||
provisioner_daemons.tags <@ $4 :: jsonb
|
||||
RETURNING id, created_at, name, provisioners, replica_id, tags, last_seen_at, version, api_version
|
||||
RETURNING id, created_at, name, provisioners, replica_id, tags, last_seen_at, version, api_version, organization_id
|
||||
`
|
||||
|
||||
type UpsertProvisionerDaemonParams struct {
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
Name string `db:"name" json:"name"`
|
||||
Provisioners []ProvisionerType `db:"provisioners" json:"provisioners"`
|
||||
Tags StringMap `db:"tags" json:"tags"`
|
||||
LastSeenAt sql.NullTime `db:"last_seen_at" json:"last_seen_at"`
|
||||
Version string `db:"version" json:"version"`
|
||||
APIVersion string `db:"api_version" json:"api_version"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
Name string `db:"name" json:"name"`
|
||||
Provisioners []ProvisionerType `db:"provisioners" json:"provisioners"`
|
||||
Tags StringMap `db:"tags" json:"tags"`
|
||||
LastSeenAt sql.NullTime `db:"last_seen_at" json:"last_seen_at"`
|
||||
Version string `db:"version" json:"version"`
|
||||
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
|
||||
APIVersion string `db:"api_version" json:"api_version"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) UpsertProvisionerDaemon(ctx context.Context, arg UpsertProvisionerDaemonParams) (ProvisionerDaemon, error) {
|
||||
|
@ -3795,6 +3799,7 @@ func (q *sqlQuerier) UpsertProvisionerDaemon(ctx context.Context, arg UpsertProv
|
|||
arg.Tags,
|
||||
arg.LastSeenAt,
|
||||
arg.Version,
|
||||
arg.OrganizationID,
|
||||
arg.APIVersion,
|
||||
)
|
||||
var i ProvisionerDaemon
|
||||
|
@ -3808,6 +3813,7 @@ func (q *sqlQuerier) UpsertProvisionerDaemon(ctx context.Context, arg UpsertProv
|
|||
&i.LastSeenAt,
|
||||
&i.Version,
|
||||
&i.APIVersion,
|
||||
&i.OrganizationID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ INSERT INTO
|
|||
tags,
|
||||
last_seen_at,
|
||||
"version",
|
||||
organization_id,
|
||||
api_version
|
||||
)
|
||||
VALUES (
|
||||
|
@ -34,6 +35,7 @@ VALUES (
|
|||
@tags,
|
||||
@last_seen_at,
|
||||
@version,
|
||||
@organization_id,
|
||||
@api_version
|
||||
) ON CONFLICT("name", LOWER(COALESCE(tags ->> 'owner'::text, ''::text))) DO UPDATE SET
|
||||
provisioners = @provisioners,
|
||||
|
|
|
@ -64,3 +64,32 @@ func RequireAPIKeyOrWorkspaceAgent() func(http.Handler) http.Handler {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
// RequireAPIKeyOrProvisionerDaemonAuth is middleware that should be inserted
|
||||
// after optional ExtractAPIKey and ExtractProvisionerDaemonAuthenticated
|
||||
// middlewares to ensure one of the two authentication methods is provided.
|
||||
//
|
||||
// If both are provided, an error is returned to avoid misuse.
|
||||
func RequireAPIKeyOrProvisionerDaemonAuth() func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, hasAPIKey := APIKeyOptional(r)
|
||||
hasProvisionerDaemon := ProvisionerDaemonAuthenticated(r)
|
||||
|
||||
if hasAPIKey && hasProvisionerDaemon {
|
||||
httpapi.Write(r.Context(), w, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "API key and external provisioner authentication provided, but only one is allowed",
|
||||
})
|
||||
return
|
||||
}
|
||||
if !hasAPIKey && !hasProvisionerDaemon {
|
||||
httpapi.Write(r.Context(), w, http.StatusUnauthorized, codersdk.Response{
|
||||
Message: "API key or external provisioner authentication required, but none provided",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
package httpmw
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/subtle"
|
||||
"net/http"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"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/codersdk"
|
||||
)
|
||||
|
||||
type provisionerDaemonContextKey struct{}
|
||||
|
||||
func ProvisionerDaemonAuthenticated(r *http.Request) bool {
|
||||
proxy, ok := r.Context().Value(workspaceProxyContextKey{}).(bool)
|
||||
return ok && proxy
|
||||
}
|
||||
|
||||
type ExtractProvisionerAuthConfig struct {
|
||||
DB database.Store
|
||||
Optional bool
|
||||
}
|
||||
|
||||
func ExtractProvisionerDaemonAuthenticated(opts ExtractProvisionerAuthConfig, psk string) 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()
|
||||
if psk == "" {
|
||||
if opts.Optional {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
// No psk means external provisioner daemons are not allowed.
|
||||
// So their auth is not valid.
|
||||
httpapi.Write(ctx, w, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "External provisioner daemons not enabled",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
token := r.Header.Get(codersdk.ProvisionerDaemonPSK)
|
||||
if token == "" {
|
||||
if opts.Optional {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
httpapi.Write(ctx, w, http.StatusUnauthorized, codersdk.Response{
|
||||
Message: "provisioner daemon auth token required",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if subtle.ConstantTimeCompare([]byte(token), []byte(psk)) != 1 {
|
||||
httpapi.Write(ctx, w, http.StatusUnauthorized, codersdk.Response{
|
||||
Message: "provisioner daemon auth token 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)
|
||||
ctx = dbauthz.AsProvisionerd(ctx)
|
||||
subj, ok := dbauthz.ActorFromContext(ctx)
|
||||
if !ok {
|
||||
// This should never happen
|
||||
httpapi.InternalServerError(w, xerrors.New("developer error: ExtractProvisionerDaemonAuth missing rbac actor"))
|
||||
}
|
||||
|
||||
// Use the same subject for the userAuthKey
|
||||
ctx = context.WithValue(ctx, userAuthKey{}, Authorization{
|
||||
Actor: subj,
|
||||
ActorName: "provisioner_daemon",
|
||||
})
|
||||
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
|
@ -292,6 +292,15 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
|
|||
r.Route("/organizations/{organization}/provisionerdaemons", func(r chi.Router) {
|
||||
r.Use(
|
||||
api.provisionerDaemonsEnabledMW,
|
||||
apiKeyMiddlewareOptional,
|
||||
httpmw.ExtractProvisionerDaemonAuthenticated(httpmw.ExtractProvisionerAuthConfig{
|
||||
DB: api.Database,
|
||||
Optional: true,
|
||||
}, api.ProvisionerDaemonPSK),
|
||||
// Either a user auth or provisioner auth is required
|
||||
// to move forward.
|
||||
httpmw.RequireAPIKeyOrProvisionerDaemonAuth(),
|
||||
httpmw.ExtractOrganizationParam(api.Database),
|
||||
)
|
||||
r.With(apiKeyMiddleware).Get("/", api.provisionerDaemons)
|
||||
r.With(apiKeyMiddlewareOptional).Get("/serve", api.provisionerDaemonServe)
|
||||
|
|
|
@ -86,11 +86,8 @@ func (api *API) provisionerDaemons(rw http.ResponseWriter, r *http.Request) {
|
|||
})
|
||||
return
|
||||
}
|
||||
apiDaemons := make([]codersdk.ProvisionerDaemon, 0)
|
||||
for _, daemon := range daemons {
|
||||
apiDaemons = append(apiDaemons, db2sdk.ProvisionerDaemon(daemon))
|
||||
}
|
||||
httpapi.Write(ctx, rw, http.StatusOK, apiDaemons)
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, db2sdk.List(daemons, db2sdk.ProvisionerDaemon))
|
||||
}
|
||||
|
||||
type provisionerDaemonAuth struct {
|
||||
|
@ -140,6 +137,7 @@ func (p *provisionerDaemonAuth) authorize(r *http.Request, tags map[string]strin
|
|||
// @Router /organizations/{organization}/provisionerdaemons/serve [get]
|
||||
func (api *API) provisionerDaemonServe(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
organization := httpmw.OrganizationParam(r)
|
||||
|
||||
tags := map[string]string{}
|
||||
if r.URL.Query().Has("tag") {
|
||||
|
@ -252,13 +250,14 @@ func (api *API) provisionerDaemonServe(rw http.ResponseWriter, r *http.Request)
|
|||
// Create the daemon in the database.
|
||||
now := dbtime.Now()
|
||||
daemon, err := api.Database.UpsertProvisionerDaemon(authCtx, database.UpsertProvisionerDaemonParams{
|
||||
Name: name,
|
||||
Provisioners: provisioners,
|
||||
Tags: tags,
|
||||
CreatedAt: now,
|
||||
LastSeenAt: sql.NullTime{Time: now, Valid: true},
|
||||
Version: versionHdrVal,
|
||||
APIVersion: apiVersion,
|
||||
Name: name,
|
||||
Provisioners: provisioners,
|
||||
Tags: tags,
|
||||
CreatedAt: now,
|
||||
LastSeenAt: sql.NullTime{Time: now, Valid: true},
|
||||
Version: versionHdrVal,
|
||||
APIVersion: apiVersion,
|
||||
OrganizationID: organization.ID,
|
||||
})
|
||||
if err != nil {
|
||||
if !xerrors.Is(err, context.Canceled) {
|
||||
|
|
Loading…
Reference in New Issue