Skip to content

Commit

Permalink
sql: Add CREATE JOINTOKEN statement, gated behind feature flag
Browse files Browse the repository at this point in the history
Adds a CREATE JOINTOKEN statement for use in TLS auto-joins.
This statement, when run on a self-hosted single-tenant
Cockroach node, creates and returns a new join token. This
join token can then be copy-pasted to new nodes and used
to give them the set of certificates for secure auto TLS
initialization.

See RFC #51991. Part of #60632.

Release justification: New code path, gated behind an
experimental feature flag.
Release note: None.
  • Loading branch information
itsbilal committed Mar 10, 2021
1 parent 28831e8 commit 89f1c2f
Show file tree
Hide file tree
Showing 11 changed files with 225 additions and 4 deletions.
1 change: 1 addition & 0 deletions docs/generated/sql/bnf/stmt_block.bnf
Original file line number Diff line number Diff line change
Expand Up @@ -913,6 +913,7 @@ unreserved_keyword ::=
| 'JOB'
| 'JOBS'
| 'JSON'
| 'JOINTOKEN'
| 'KEY'
| 'KEYS'
| 'KMS'
Expand Down
5 changes: 5 additions & 0 deletions pkg/sql/catalog/colinfo/result_columns.go
Original file line number Diff line number Diff line change
Expand Up @@ -273,3 +273,8 @@ var ExportColumns = ResultColumns{
{Name: "rows", Typ: types.Int},
{Name: "bytes", Typ: types.Int},
}

// CreateJoinTokenColumns are the result columns of a CREATE JOINTOKEN statement.
var CreateJoinTokenColumns = ResultColumns{
{Name: "jointoken", Typ: types.String},
}
76 changes: 73 additions & 3 deletions pkg/sql/join_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,16 @@

package sql

import "github.com/cockroachdb/cockroach/pkg/settings"
import (
"context"

"github.com/cockroachdb/cockroach/pkg/featureflag"
"github.com/cockroachdb/cockroach/pkg/server/serverpb"
"github.com/cockroachdb/cockroach/pkg/settings"
"github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgcode"
"github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgerror"
"github.com/cockroachdb/cockroach/pkg/sql/sem/tree"
)

// FeatureTLSAutoJoinEnabled is used to enable and disable the TLS auto-join
// feature.
Expand All @@ -20,5 +29,66 @@ var FeatureTLSAutoJoinEnabled = settings.RegisterBoolSetting(
false,
)

// TODO(bilal): Implement a CREATE JOIN TOKEN statement, gated by
// FeatureTLSAutoJoinEnabled.
type createJoinTokenNode struct {
optColumnsSlot

status serverpb.NodesStatusServer
token string
nexted bool
}

func (c *createJoinTokenNode) startExec(params runParams) error {
token, err := c.status.GenerateJoinToken(params.ctx)
if err != nil {
return err
}
c.token = token
return nil
}

func (c *createJoinTokenNode) Next(runParams) (bool, error) {
if len(c.token) > 0 && !c.nexted {
c.nexted = true
return true, nil
}
return false, nil
}

func (c *createJoinTokenNode) Close(context.Context) {
c.token = ""
c.nexted = false
}

func (c *createJoinTokenNode) Values() tree.Datums {
if len(c.token) == 0 {
return tree.Datums{tree.DNull}
}
return tree.Datums{tree.NewDString(c.token)}
}

// CreateJoinToken creates a join token creation node.
func (p *planner) CreateJoinToken(ctx context.Context, _ *tree.CreateJoinToken) (planNode, error) {
hasAdmin, err := p.HasAdminRole(ctx)
if err != nil {
return nil, err
}
if !hasAdmin {
return nil, pgerror.New(pgcode.InsufficientPrivilege, "must be superuser to create join token")
}
if err := featureflag.CheckEnabled(
ctx, p.ExecCfg(), FeatureTLSAutoJoinEnabled, "TLS auto join"); err != nil {
return nil, pgerror.New(
pgcode.FeatureNotSupported,
err.Error(),
)
}
if n, err := p.extendedEvalCtx.NodesStatusServer.OptionalNodesStatusServer(47900); err == nil && n != nil {
return &createJoinTokenNode{
status: n,
}, nil
}
return nil, pgerror.New(
pgcode.FeatureNotSupported,
"unsupported statement",
)
}
98 changes: 98 additions & 0 deletions pkg/sql/join_token_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Copyright 2021 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

package sql

import (
"context"
"testing"

"github.com/cockroachdb/cockroach/pkg/base"
"github.com/cockroachdb/cockroach/pkg/server/serverpb"
"github.com/cockroachdb/cockroach/pkg/settings/cluster"
"github.com/cockroachdb/cockroach/pkg/sql/sem/tree"
"github.com/cockroachdb/cockroach/pkg/testutils/serverutils"
"github.com/cockroachdb/cockroach/pkg/util/leaktest"
"github.com/cockroachdb/cockroach/pkg/util/log"
"github.com/cockroachdb/errors"
"github.com/stretchr/testify/require"
)

type fakeStatusServer struct {
token string
err error
}

func (f *fakeStatusServer) Nodes(
ctx context.Context, request *serverpb.NodesRequest,
) (*serverpb.NodesResponse, error) {
panic("unimplemented")
}

func (f *fakeStatusServer) GenerateJoinToken(ctx context.Context) (string, error) {
return f.token, f.err
}

var _ serverpb.NodesStatusServer = &fakeStatusServer{}

func TestCreateJoinTokenNode(t *testing.T) {
defer leaktest.AfterTest(t)()
defer log.Scope(t).Close(t)

f := &fakeStatusServer{}
c := &createJoinTokenNode{
status: f,
}

errInjected := errors.New("injected error")
f.err = errInjected
err := c.startExec(runParams{})
require.EqualError(t, err, errInjected.Error())

f.err = nil
f.token = "testtoken"
*c = createJoinTokenNode{
status: f,
}
require.NoError(t, c.startExec(runParams{}))
ok, err := c.Next(runParams{})
require.True(t, ok)
require.NoError(t, err)
val := c.Values()
require.NotEmpty(t, val)
require.Equal(t, tree.DString("testtoken"), *(val[0].(*tree.DString)))
ok, err = c.Next(runParams{})
require.False(t, ok)
require.NoError(t, err)
}

func TestCreateJoinTokenStmt(t *testing.T) {
defer leaktest.AfterTest(t)()
defer log.Scope(t).Close(t)

settings := cluster.MakeTestingClusterSettings()
FeatureTLSAutoJoinEnabled.Override(&settings.SV, true)
s, sqldb, _ := serverutils.StartServer(t, base.TestServerArgs{
Settings: settings,
})
defer s.Stopper().Stop(context.Background())

rows, err := sqldb.Query("CREATE JOINTOKEN;")
require.NoError(t, err)
count := 0
for rows.Next() {
count++
var jt string
require.NoError(t, rows.Scan(&jt))
require.NotEmpty(t, jt)
}
require.Equal(t, 1, count)
require.NoError(t, rows.Close())
}
3 changes: 3 additions & 0 deletions pkg/sql/opaque.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ func planOpaque(ctx context.Context, p *planner, stmt tree.Statement) (planNode,
return p.CreateDatabase(ctx, n)
case *tree.CreateIndex:
return p.CreateIndex(ctx, n)
case *tree.CreateJoinToken:
return p.CreateJoinToken(ctx, n)
case *tree.CreateSchema:
return p.CreateSchema(ctx, n)
case *tree.CreateType:
Expand Down Expand Up @@ -235,6 +237,7 @@ func init() {
&tree.CreateDatabase{},
&tree.CreateExtension{},
&tree.CreateIndex{},
&tree.CreateJoinToken{},
&tree.CreateSchema{},
&tree.CreateSequence{},
&tree.CreateType{},
Expand Down
13 changes: 12 additions & 1 deletion pkg/sql/parser/sql.y
Original file line number Diff line number Diff line change
Expand Up @@ -650,7 +650,7 @@ func (u *sqlSymUnion) objectNamePrefixList() tree.ObjectNamePrefixList {
%token <str> INNER INSERT INT INTEGER
%token <str> INTERSECT INTERVAL INTO INTO_DB INVERTED IS ISERROR ISNULL ISOLATION

%token <str> JOB JOBS JOIN JSON JSONB JSON_SOME_EXISTS JSON_ALL_EXISTS
%token <str> JOB JOBS JOIN JOINTOKEN JSON JSONB JSON_SOME_EXISTS JSON_ALL_EXISTS

%token <str> KEY KEYS KMS KV

Expand Down Expand Up @@ -829,6 +829,7 @@ func (u *sqlSymUnion) objectNamePrefixList() tree.ObjectNamePrefixList {
%type <tree.Statement> create_table_as_stmt
%type <tree.Statement> create_view_stmt
%type <tree.Statement> create_sequence_stmt
%type <tree.Statement> create_join_token_stmt

%type <tree.Statement> create_stats_stmt
%type <*tree.CreateStatsOptions> opt_create_stats_options
Expand Down Expand Up @@ -3159,6 +3160,7 @@ create_stmt:
| create_changefeed_stmt
| create_replication_stream_stmt
| create_extension_stmt // EXTEND WITH HELP: CREATE EXTENSION
| create_join_token_stmt // help text to be added after feature flag removed
| create_unsupported {}
| CREATE error // SHOW HELP: CREATE

Expand Down Expand Up @@ -3414,6 +3416,14 @@ create_replication_stream_stmt:
}
}

// Create join token statement. Help text to come when feature flag is dropped.
create_join_token_stmt:
CREATE JOINTOKEN
{
/* SKIP DOC */
$$.val = &tree.CreateJoinToken{}
}

// Optional replication stream options.
opt_with_replication_options:
WITH replication_options_list
Expand Down Expand Up @@ -12523,6 +12533,7 @@ unreserved_keyword:
| JOB
| JOBS
| JSON
| JOINTOKEN
| KEY
| KEYS
| KMS
Expand Down
1 change: 1 addition & 0 deletions pkg/sql/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ var _ planNode = &cancelSessionsNode{}
var _ planNode = &changePrivilegesNode{}
var _ planNode = &createDatabaseNode{}
var _ planNode = &createIndexNode{}
var _ planNode = &createJoinTokenNode{}
var _ planNode = &createSequenceNode{}
var _ planNode = &createStatsNode{}
var _ planNode = &createTableNode{}
Expand Down
2 changes: 2 additions & 0 deletions pkg/sql/plan_columns.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ func getPlanColumns(plan planNode, mut bool) colinfo.ResultColumns {
return n.getColumns(mut, colinfo.SequenceSelectColumns)
case *exportNode:
return n.getColumns(mut, colinfo.ExportColumns)
case *createJoinTokenNode:
return n.getColumns(mut, colinfo.CreateJoinTokenColumns)

// The columns in the hookFnNode are returned by the hook function; we don't
// know if they can be modified in place or not.
Expand Down
22 changes: 22 additions & 0 deletions pkg/sql/sem/tree/join_token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright 2021 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

package tree

// CreateJoinToken represents a CREATE JOINTOKEN statement.
type CreateJoinToken struct {
}

var _ Statement = &CreateJoinToken{}

// Format implements the NodeFormatter interface.
func (c *CreateJoinToken) Format(ctx *FmtCtx) {
ctx.WriteString("CREATE JOINTOKEN")
}
7 changes: 7 additions & 0 deletions pkg/sql/sem/tree/stmt.go
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,12 @@ func (*CreateIndex) StatementType() StatementType { return DDL }
// StatementTag returns a short string identifying the type of statement.
func (*CreateIndex) StatementTag() string { return "CREATE INDEX" }

// StatementType implements the Statement interface.
func (*CreateJoinToken) StatementType() StatementType { return Rows }

// StatementTag returns a short string identifying the type of statement.
func (*CreateJoinToken) StatementTag() string { return "CREATE JOINTOKEN" }

// StatementType implements the Statement interface.
func (n *CreateSchema) StatementType() StatementType { return DDL }

Expand Down Expand Up @@ -1130,6 +1136,7 @@ func (n *CreateChangefeed) String() string { return AsString(n) }
func (n *CreateDatabase) String() string { return AsString(n) }
func (n *CreateExtension) String() string { return AsString(n) }
func (n *CreateIndex) String() string { return AsString(n) }
func (n *CreateJoinToken) String() string { return AsString(n) }
func (n *CreateRole) String() string { return AsString(n) }
func (n *CreateTable) String() string { return AsString(n) }
func (n *CreateSchema) String() string { return AsString(n) }
Expand Down
1 change: 1 addition & 0 deletions pkg/sql/walk.go
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,7 @@ var planNodeNames = map[reflect.Type]string{
reflect.TypeOf(&createDatabaseNode{}): "create database",
reflect.TypeOf(&createExtensionNode{}): "create extension",
reflect.TypeOf(&createIndexNode{}): "create index",
reflect.TypeOf(&createJoinTokenNode{}): "create jointoken",
reflect.TypeOf(&createSequenceNode{}): "create sequence",
reflect.TypeOf(&createSchemaNode{}): "create schema",
reflect.TypeOf(&createStatsNode{}): "create statistics",
Expand Down

0 comments on commit 89f1c2f

Please sign in to comment.