sourcegraph/internal/database/executor_secrets.go
Erik Seliger f09ea82e7c
Enable use of executor secrets for SSBC (#43940)
This PR makes use of the generic executor secrets infrastructure in SSBC! We will now read the secret values for resolving workspaces and when spawning an executor job, to make sure that everything is set as required and that the cache keys are properly calculated so that a secret value change would invalidate the cache.

For this to have any effect, src-cli will need to be bumped to a new version. It will not break existing executions though so this is safe to merge.
2022-11-07 20:26:41 +01:00

637 lines
17 KiB
Go

package database
import (
"context"
"database/sql"
"fmt"
"time"
"github.com/keegancsmith/sqlf"
"github.com/lib/pq"
"github.com/sourcegraph/log"
"github.com/sourcegraph/sourcegraph/internal/actor"
"github.com/sourcegraph/sourcegraph/internal/database/basestore"
"github.com/sourcegraph/sourcegraph/internal/database/dbutil"
"github.com/sourcegraph/sourcegraph/internal/encryption"
"github.com/sourcegraph/sourcegraph/internal/errcode"
"github.com/sourcegraph/sourcegraph/internal/timeutil"
"github.com/sourcegraph/sourcegraph/lib/errors"
)
// ExecutorSecret represents a row in the `executor_secrets` table.
type ExecutorSecret struct {
ID int64
Key string
Scope string
CreatorID int32
NamespaceUserID int32
NamespaceOrgID int32
CreatedAt time.Time
UpdatedAt time.Time
// unexported so that there's no direct access. Use `Value` to access it
// which will generate the access log entries as well.
encryptedValue *encryption.Encryptable
}
// Value decrypts the contained value and logs an access log event. Calling Value
// multiple times will not require another decryption call, but will create an
// additional access log entry.
func (e ExecutorSecret) Value(ctx context.Context, s ExecutorSecretAccessLogStore) (string, error) {
if err := s.Create(ctx, &ExecutorSecretAccessLog{
// user is set automatically from the context actor.
ExecutorSecretID: e.ID,
}); err != nil {
return "", errors.Wrap(err, "creating secret access log entry")
}
return e.encryptedValue.Decrypt(ctx)
}
type ExecutorSecretScope string
const (
ExecutorSecretScopeBatches = "batches"
)
// ExecutorSecretNotFoundErr is returned when a secret cannot be found.
type ExecutorSecretNotFoundErr struct {
id int64
}
func (err ExecutorSecretNotFoundErr) Error() string {
return fmt.Sprintf("executor secret not found: id=%d", err.id)
}
func (ExecutorSecretNotFoundErr) NotFound() bool {
return true
}
// ExecutorSecretStore provides access to the `executor_secrets` table.
type ExecutorSecretStore interface {
basestore.ShareableStore
With(basestore.ShareableStore) ExecutorSecretStore
Transact(context.Context) (ExecutorSecretStore, error)
Done(err error) error
ExecResult(ctx context.Context, query *sqlf.Query) (sql.Result, error)
// Create inserts the given ExecutorSecret into the database.
Create(ctx context.Context, scope ExecutorSecretScope, secret *ExecutorSecret, value string) error
// Update updates a secret in the database. If the secret cannot be found,
// an error is returned.
Update(ctx context.Context, scope ExecutorSecretScope, secret *ExecutorSecret, value string) error
// Delete deletes the given executor secret.
Delete(ctx context.Context, scope ExecutorSecretScope, id int64) error
// GetByID returns the executor secret matching the given ID, or
// ExecutorSecretNotFoundErr if no such secret exists.
GetByID(ctx context.Context, scope ExecutorSecretScope, id int64) (*ExecutorSecret, error)
// List returns all secrets matching the given options.
List(context.Context, ExecutorSecretScope, ExecutorSecretsListOpts) ([]*ExecutorSecret, int, error)
// Count counts all secrets matching the given options.
Count(context.Context, ExecutorSecretScope, ExecutorSecretsListOpts) (int, error)
}
// ExecutorSecretsListOpts provide the options when listing secrets. If no namespace
// scoping is provided, only global credentials are returned (no namespace set).
type ExecutorSecretsListOpts struct {
*LimitOffset
// Keys, if set limits the returned secrets to the list of provided keys.
Keys []string
// NamespaceUserID, when set, returns secrets accessible in the user namespace.
// These may include global secrets.
NamespaceUserID int32
// NamespaceOrgID, when set, returns secrets accessible in the user namespace.
// These may include global secrets.
NamespaceOrgID int32
}
func (opts ExecutorSecretsListOpts) sqlConds(ctx context.Context, scope ExecutorSecretScope) (*sqlf.Query, error) {
authz, err := executorSecretsAuthzQueryConds(ctx)
if err != nil {
return nil, err
}
globalSecret := sqlf.Sprintf("namespace_user_id IS NULL AND namespace_org_id IS NULL")
preds := []*sqlf.Query{
authz,
sqlf.Sprintf("scope = %s", scope),
}
if opts.NamespaceOrgID != 0 {
preds = append(preds, sqlf.Sprintf("(namespace_org_id = %s OR (%s))", opts.NamespaceOrgID, globalSecret))
} else if opts.NamespaceUserID != 0 {
preds = append(preds, sqlf.Sprintf("(namespace_user_id = %s OR (%s))", opts.NamespaceUserID, globalSecret))
} else {
preds = append(preds, globalSecret)
}
if len(opts.Keys) > 0 {
preds = append(preds, sqlf.Sprintf("key = ANY(%s)", pq.Array(opts.Keys)))
}
return sqlf.Join(preds, "\n AND "), nil
}
// limitSQL overrides LimitOffset.SQL() to give a LIMIT clause with one extra value
// so we can populate the next cursor.
func (opts *ExecutorSecretsListOpts) limitSQL() *sqlf.Query {
if opts.LimitOffset == nil || opts.Limit == 0 {
return &sqlf.Query{}
}
return (&LimitOffset{Limit: opts.Limit + 1, Offset: opts.Offset}).SQL()
}
type executorSecretStore struct {
logger log.Logger
*basestore.Store
key encryption.Key
}
// ExecutorSecretsWith instantiates and returns a new ExecutorSecretStore using the other store handle.
func ExecutorSecretsWith(logger log.Logger, other basestore.ShareableStore, key encryption.Key) ExecutorSecretStore {
return &executorSecretStore{
logger: logger,
Store: basestore.NewWithHandle(other.Handle()),
key: key,
}
}
func (s *executorSecretStore) With(other basestore.ShareableStore) ExecutorSecretStore {
return &executorSecretStore{
logger: s.logger,
Store: s.Store.With(other),
key: s.key,
}
}
func (s *executorSecretStore) Transact(ctx context.Context) (ExecutorSecretStore, error) {
txBase, err := s.Store.Transact(ctx)
return &executorSecretStore{
logger: s.logger,
Store: txBase,
key: s.key,
}, err
}
var ErrEmptyExecutorSecretKey = errors.New("empty executor secret key is not allowed")
var ErrEmptyExecutorSecretValue = errors.New("empty executor secret value is not allowed")
func (s *executorSecretStore) Create(ctx context.Context, scope ExecutorSecretScope, secret *ExecutorSecret, value string) error {
if len(secret.Key) == 0 {
return ErrEmptyExecutorSecretKey
}
if len(value) == 0 {
return ErrEmptyExecutorSecretValue
}
// SECURITY: check that the current user is authorized to create a secret for the given namespace.
if err := ensureActorHasNamespaceWriteAccess(ctx, NewDBWith(s.logger, s), secret); err != nil {
return err
}
// Set the current actor as the secret creator if not set.
if secret.CreatorID == 0 {
secret.CreatorID = actor.FromContext(ctx).UID
}
encryptedValue, keyID, err := encryptExecutorSecret(ctx, s.key, value)
if err != nil {
return err
}
q := sqlf.Sprintf(
executorSecretCreateQueryFmtstr,
scope,
secret.Key,
encryptedValue, // N.B.: is already a []byte
keyID,
dbutil.NewNullInt(int(secret.NamespaceUserID)),
dbutil.NewNullInt(int(secret.NamespaceOrgID)),
secret.CreatorID,
sqlf.Join(executorSecretsColumns, ", "),
)
row := s.QueryRow(ctx, q)
if err := scanExecutorSecret(secret, s.key, row); err != nil {
return err
}
return nil
}
func (s *executorSecretStore) Update(ctx context.Context, scope ExecutorSecretScope, secret *ExecutorSecret, value string) error {
if len(value) == 0 {
return ErrEmptyExecutorSecretValue
}
// SECURITY: check that the current user is authorized to update a secret in the given namespace.
if err := ensureActorHasNamespaceWriteAccess(ctx, NewDBWith(s.logger, s), secret); err != nil {
return err
}
secret.UpdatedAt = timeutil.Now()
encryptedValue, keyID, err := encryptExecutorSecret(ctx, s.key, value)
if err != nil {
return err
}
authz, err := executorSecretsAuthzQueryConds(ctx)
if err != nil {
return err
}
q := sqlf.Sprintf(
executorSecretUpdateQueryFmtstr,
encryptedValue,
keyID,
secret.UpdatedAt,
secret.ID,
scope,
authz,
sqlf.Join(executorSecretsColumns, ", "),
)
row := s.QueryRow(ctx, q)
if err := scanExecutorSecret(secret, s.key, row); err != nil {
return err
}
return nil
}
func (s *executorSecretStore) Delete(ctx context.Context, scope ExecutorSecretScope, id int64) error {
// Grab the secret and make sure that namespace write access from the actor
// is acceptable.
var tx ExecutorSecretStore
if s.InTransaction() {
tx = s
} else {
var err error
tx, err = s.Transact(ctx)
if err != nil {
return err
}
defer func() { err = tx.Done(err) }()
}
secret, err := tx.GetByID(ctx, scope, id)
if err != nil {
return err
}
// SECURITY: check that the current user is authorized to delete a secret in the given namespace.
if err := ensureActorHasNamespaceWriteAccess(ctx, NewDBWith(s.logger, tx), secret); err != nil {
return err
}
authz, err := executorSecretsAuthzQueryConds(ctx)
if err != nil {
return err
}
q := sqlf.Sprintf("DELETE FROM executor_secrets WHERE id = %s AND scope = %s AND %s", id, scope, authz)
res, err := tx.ExecResult(ctx, q)
if err != nil {
return err
}
if rows, err := res.RowsAffected(); err != nil {
return err
} else if rows == 0 {
return ExecutorSecretNotFoundErr{id: id}
}
return nil
}
func (s *executorSecretStore) GetByID(ctx context.Context, scope ExecutorSecretScope, id int64) (*ExecutorSecret, error) {
authz, err := executorSecretsAuthzQueryConds(ctx)
if err != nil {
return nil, err
}
q := sqlf.Sprintf(
"SELECT %s FROM executor_secrets WHERE id = %s AND %s",
sqlf.Join(executorSecretsColumns, ", "),
id,
authz,
)
secret := ExecutorSecret{}
row := s.QueryRow(ctx, q)
if err := scanExecutorSecret(&secret, s.key, row); err == sql.ErrNoRows {
return nil, ExecutorSecretNotFoundErr{id: id}
} else if err != nil {
return nil, err
}
return &secret, nil
}
func (s *executorSecretStore) List(ctx context.Context, scope ExecutorSecretScope, opts ExecutorSecretsListOpts) ([]*ExecutorSecret, int, error) {
conds, err := opts.sqlConds(ctx, scope)
if err != nil {
return nil, 0, err
}
q := sqlf.Sprintf(
executorSecretsListQueryFmtstr,
sqlf.Join(executorSecretsColumns, ", "),
sqlf.Join(executorSecretsColumns, ", "),
conds,
opts.limitSQL(),
)
rows, err := s.Query(ctx, q)
if err != nil {
return nil, 0, err
}
defer func() { err = basestore.CloseRows(rows, err) }()
var secrets []*ExecutorSecret
for rows.Next() {
secret := ExecutorSecret{}
if err := scanExecutorSecret(&secret, s.key, rows); err != nil {
return nil, 0, err
}
secrets = append(secrets, &secret)
}
// Check if there were more results than the limit: if so, then we need to
// set the return cursor and lop off the extra secret that we retrieved.
next := 0
if opts.LimitOffset != nil && opts.Limit != 0 && len(secrets) == opts.Limit+1 {
next = opts.Offset + opts.Limit
secrets = secrets[:len(secrets)-1]
}
return secrets, next, nil
}
func (s *executorSecretStore) Count(ctx context.Context, scope ExecutorSecretScope, opts ExecutorSecretsListOpts) (int, error) {
conds, err := opts.sqlConds(ctx, scope)
if err != nil {
return 0, err
}
q := sqlf.Sprintf(
executorSecretsCountQueryFmtstr,
conds,
)
totalCount, _, err := basestore.ScanFirstInt(s.Query(ctx, q))
if err != nil {
return 0, err
}
return totalCount, nil
}
// executorSecretsColumns are the columns that must be selected by
// executor_secrets queries in order to use scanExecutorSecret().
var executorSecretsColumns = []*sqlf.Query{
sqlf.Sprintf("id"),
sqlf.Sprintf("scope"),
sqlf.Sprintf("key"),
sqlf.Sprintf("value"),
sqlf.Sprintf("encryption_key_id"),
sqlf.Sprintf("namespace_user_id"),
sqlf.Sprintf("namespace_org_id"),
sqlf.Sprintf("creator_id"),
sqlf.Sprintf("created_at"),
sqlf.Sprintf("updated_at"),
}
const executorSecretsListQueryFmtstr = `
SELECT %s
FROM (
SELECT
%s,
RANK() OVER(
PARTITION BY key
ORDER BY
namespace_user_id NULLS LAST,
namespace_org_id NULLS LAST
)
FROM executor_secrets
WHERE %s
) executor_secrets
WHERE
executor_secrets.rank = 1
ORDER BY key ASC
%s -- LIMIT clause
`
const executorSecretsCountQueryFmtstr = `
SELECT COUNT(*)
FROM (
SELECT
RANK() OVER(
PARTITION BY key
ORDER BY
namespace_user_id NULLS LAST,
namespace_org_id NULLS LAST
)
FROM executor_secrets
WHERE %s
) executor_secrets
WHERE
executor_secrets.rank = 1
`
const executorSecretCreateQueryFmtstr = `
INSERT INTO
executor_secrets (
scope,
key,
value,
encryption_key_id,
namespace_user_id,
namespace_org_id,
creator_id,
created_at,
updated_at
)
VALUES (
%s,
%s,
%s,
%s,
%s,
%s,
%s,
NOW(),
NOW()
)
RETURNING %s
`
const executorSecretUpdateQueryFmtstr = `
UPDATE executor_secrets
SET
value = %s,
encryption_key_id = %s,
updated_at = %s
WHERE
id = %s AND
scope = %s AND
%s -- authz query conds
RETURNING %s
`
// scanExecutorSecret scans a secret from the given scanner into the given
// ExecutorSecret.
func scanExecutorSecret(secret *ExecutorSecret, key encryption.Key, s interface {
Scan(...any) error
}) error {
var (
value []byte
keyID string
)
if err := s.Scan(
&secret.ID,
&secret.Scope,
&secret.Key,
&value,
&dbutil.NullString{S: &keyID},
&dbutil.NullInt32{N: &secret.NamespaceUserID},
&dbutil.NullInt32{N: &secret.NamespaceOrgID},
&dbutil.NullInt32{N: &secret.CreatorID},
&secret.CreatedAt,
&secret.UpdatedAt,
); err != nil {
return err
}
secret.encryptedValue = NewEncryptedCredential(string(value), keyID, key)
return nil
}
func ensureActorHasNamespaceWriteAccess(ctx context.Context, db DB, secret *ExecutorSecret) error {
a := actor.FromContext(ctx)
if a.IsInternal() {
return nil
}
if !a.IsAuthenticated() {
return errors.New("not logged in")
}
// TODO: This should use the helpers from the auth package, but that package
// today depends on the database package, so that would be an import cycle.
if secret.NamespaceOrgID != 0 {
// Check if the current user is org member.
resp, err := db.OrgMembers().GetByOrgIDAndUserID(ctx, secret.NamespaceOrgID, a.UID)
if err != nil {
if !errcode.IsNotFound(err) {
return err
}
// Not found case: Fall through and eventually end up down at the site-admin
// check.
}
// If membership is found, the user may pass.
if resp != nil {
return nil
}
// Not a member case: Fall through and eventually end up down at the site-admin
// check.
} else if secret.NamespaceUserID != 0 {
// If the actor is the same user as the namespace user, pass. Otherwise
// fall through and check if they're site-admin.
if a.UID == secret.NamespaceUserID {
return nil
}
}
// Check user is site admin.
user, err := db.Users().GetByID(ctx, a.UID)
if err != nil {
return err
}
if user == nil || !user.SiteAdmin {
return errors.New("not site-admin")
}
return nil
}
// executorSecretsAuthzQueryConds generates authz query conditions for checking
// access to the secret at the database level.
// Internal actors will always pass.
func executorSecretsAuthzQueryConds(ctx context.Context) (*sqlf.Query, error) {
a := actor.FromContext(ctx)
if a.IsInternal() {
return sqlf.Sprintf("(TRUE)"), nil
}
return sqlf.Sprintf(
executorSecretsAuthzQueryCondsFmtstr,
a.UID,
a.UID,
a.UID,
), nil
}
// executorSecretsAuthzQueryCondsFmtstr contains the SQL used to determine if a user
// has access to the given secret value. It is used in every query to ensure that
// the store never returns secrets that are not meant to be seen by them.
const executorSecretsAuthzQueryCondsFmtstr = `
(
(
-- the secret is a global secret
executor_secrets.namespace_user_id IS NULL
AND
executor_secrets.namespace_org_id IS NULL
)
OR
(
-- user is the same as the actor
executor_secrets.namespace_user_id = %s
)
OR
(
-- actor is part of the org
executor_secrets.namespace_org_id IS NOT NULL
AND
EXISTS (
SELECT 1
FROM orgs
JOIN org_members ON org_members.org_id = orgs.id
WHERE org_members.user_id = %s
)
)
OR
(
-- actor is site admin
EXISTS (
SELECT 1
FROM users
WHERE site_admin = TRUE AND id = %s -- actor user ID
)
)
)
`
// encryptExecutorSecret encrypts the given raw secret value if encryption is enabled
// and returns the encrypted data and the associated encryption key ID.
func encryptExecutorSecret(ctx context.Context, key encryption.Key, raw string) ([]byte, string, error) {
if len(raw) == 0 {
return nil, "", errors.New("got empty secret")
}
data, keyID, err := encryption.MaybeEncrypt(ctx, key, raw)
return []byte(data), keyID, err
}
// NewMockExecutorSecret can be used in tests to create an executor secret with a
// set inner value. DO NOT USE THIS OUTSIDE OF TESTS.
func NewMockExecutorSecret(s *ExecutorSecret, v string) *ExecutorSecret {
s.encryptedValue = NewUnencryptedCredential([]byte(v))
return s
}