mirror of
https://github.com/dolthub/dolt.git
synced 2026-05-13 03:10:03 -05:00
Merge pull request #4688 from dolthub/aaron/cluster-serve-jwks-use-jwt-on-rpcs
go/libraries/doltcore/sqle/cluster: Add the ability to generate a keypair, serve a JWKS, and sign outbound RPCs with a JWT.
This commit is contained in:
@@ -55,7 +55,7 @@ jobs:
|
||||
- name: Setup Python 3.x
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ^3.6
|
||||
python-version: "3.10"
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
@@ -104,10 +104,11 @@ jobs:
|
||||
id: parquet_cli
|
||||
working-directory: ./.ci_bin
|
||||
run: |
|
||||
git clone https://github.com/apache/parquet-mr.git
|
||||
cd parquet-mr/parquet-cli
|
||||
curl -OL https://github.com/apache/parquet-mr/archive/refs/tags/apache-parquet-1.12.3.tar.gz
|
||||
tar zxvf apache-parquet-1.12.3.tar.gz
|
||||
cd parquet-mr-apache-parquet-1.12.3/parquet-cli
|
||||
mvn clean install -DskipTests
|
||||
runtime_jar="$(pwd)"/target/parquet-cli-1.13.0-SNAPSHOT-runtime.jar
|
||||
runtime_jar="$(pwd)"/target/parquet-cli-1.12.3-runtime.jar
|
||||
echo "runtime_jar=$runtime_jar" >> $GITHUB_OUTPUT
|
||||
- name: Check expect
|
||||
run: expect -v
|
||||
|
||||
@@ -100,7 +100,7 @@ jobs:
|
||||
- name: Setup Python 3.x
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ^3.6
|
||||
python-version: "3.10"
|
||||
- uses: actions/checkout@v3
|
||||
if: ${{ github.event_name == 'repository_dispatch' }}
|
||||
with:
|
||||
|
||||
Generated
-209
@@ -5434,215 +5434,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
= LICENSE 6681c42f6974591d2056518a26201323fa7d42bdc4d64bfc12c332b3 =
|
||||
================================================================================
|
||||
|
||||
================================================================================
|
||||
= github.com/pquerna/cachecontrol licensed under: =
|
||||
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
= LICENSE 75cd5500580317e758b5e984e017524dc961140e4889f7d427f85e41 =
|
||||
================================================================================
|
||||
|
||||
================================================================================
|
||||
= github.com/prometheus/client_golang licensed under: =
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ func loadCred(dEnv *env.DoltEnv, apr *argparser.ArgParseResults) (creds.DoltCred
|
||||
}
|
||||
return dc, nil
|
||||
} else {
|
||||
dc, valid, err := dEnv.UserRPCCreds()
|
||||
dc, valid, err := dEnv.UserDoltCreds()
|
||||
if !valid {
|
||||
return creds.EmptyCreds, errhand.BuildDError("error: no user credentials found").Build()
|
||||
}
|
||||
@@ -138,7 +138,7 @@ func loadCred(dEnv *env.DoltEnv, apr *argparser.ArgParseResults) (creds.DoltCred
|
||||
func checkCredAndPrintSuccess(ctx context.Context, dEnv *env.DoltEnv, dc creds.DoltCreds, endpoint string) errhand.VerboseError {
|
||||
cfg, err := dEnv.GetGRPCDialParams(grpcendpoint.Config{
|
||||
Endpoint: endpoint,
|
||||
Creds: dc,
|
||||
Creds: dc.RPCCreds(),
|
||||
})
|
||||
if err != nil {
|
||||
return errhand.BuildDError("error: unable to build server endpoint options.").AddCause(err).Build()
|
||||
|
||||
@@ -163,7 +163,7 @@ func updateProfileWithCredentials(ctx context.Context, dEnv *env.DoltEnv, c cred
|
||||
hostAndPort := fmt.Sprintf("%s:%s", host, port)
|
||||
cfg, err := dEnv.GetGRPCDialParams(grpcendpoint.Config{
|
||||
Endpoint: hostAndPort,
|
||||
Creds: c,
|
||||
Creds: c.RPCCreds(),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error: unable to build dial options server with credentials: %w", err)
|
||||
|
||||
@@ -93,7 +93,7 @@ func (cmd LsCmd) Exec(ctx context.Context, commandStr string, args []string, dEn
|
||||
}
|
||||
|
||||
func getJWKHandler(dEnv *env.DoltEnv) func(string, int64, bool) bool {
|
||||
current, valid, _ := dEnv.UserRPCCreds()
|
||||
current, valid, _ := dEnv.UserDoltCreds()
|
||||
first := false
|
||||
return func(path string, size int64, isDir bool) (stop bool) {
|
||||
if strings.HasSuffix(path, creds.JWKFileExtension) {
|
||||
|
||||
@@ -65,7 +65,10 @@ func validateJWT(config []JwksConfig, username, identity, token string, reqTime
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
vd := jwtauth.NewJWTValidator(pr)
|
||||
vd, err := jwtauth.NewJWTValidator(pr)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
claims, err := vd.ValidateJWT(token, reqTime)
|
||||
if err != nil {
|
||||
return false, err
|
||||
|
||||
@@ -240,7 +240,7 @@ func openBrowserForCredsAdd(dc creds.DoltCreds, loginUrl string) {
|
||||
func getCredentialsClient(dEnv *env.DoltEnv, dc creds.DoltCreds, authEndpoint string, insecure bool) (remotesapi.CredentialsServiceClient, errhand.VerboseError) {
|
||||
cfg, err := dEnv.GetGRPCDialParams(grpcendpoint.Config{
|
||||
Endpoint: authEndpoint,
|
||||
Creds: dc,
|
||||
Creds: dc.RPCCreds(),
|
||||
Insecure: insecure,
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -60,7 +60,6 @@ require (
|
||||
github.com/google/flatbuffers v2.0.6+incompatible
|
||||
github.com/kch42/buzhash v0.0.0-20160816060738-9bdec3dec7c6
|
||||
github.com/mitchellh/go-ps v1.0.0
|
||||
github.com/pquerna/cachecontrol v0.1.0
|
||||
github.com/prometheus/client_golang v1.11.0
|
||||
github.com/shirou/gopsutil/v3 v3.22.1
|
||||
github.com/vbauerster/mpb v3.4.0+incompatible
|
||||
|
||||
@@ -593,8 +593,6 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/pquerna/cachecontrol v0.1.0 h1:yJMy84ti9h/+OEWa752kBTKv4XC30OtVVHYv/8cTqKc=
|
||||
github.com/pquerna/cachecontrol v0.1.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
|
||||
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||
|
||||
@@ -117,43 +117,62 @@ func (dc DoltCreds) Sign(data []byte) []byte {
|
||||
return ed25519.Sign(dc.PrivKey, data)
|
||||
}
|
||||
|
||||
func (dc DoltCreds) toBearerToken() (string, error) {
|
||||
b32KIDStr := dc.KeyIDBase32Str()
|
||||
key := jose.SigningKey{Algorithm: jose.EdDSA, Key: ed25519.PrivateKey(dc.PrivKey)}
|
||||
type RPCCreds struct {
|
||||
PrivKey ed25519.PrivateKey
|
||||
KeyID string
|
||||
Audience string
|
||||
Issuer string
|
||||
Subject string
|
||||
RequireTLS bool
|
||||
}
|
||||
|
||||
func (c *RPCCreds) toBearerToken() (string, error) {
|
||||
key := jose.SigningKey{Algorithm: jose.EdDSA, Key: c.PrivKey}
|
||||
opts := &jose.SignerOptions{ExtraHeaders: map[jose.HeaderKey]interface{}{
|
||||
JWTKIDHeader: b32KIDStr,
|
||||
JWTKIDHeader: c.KeyID,
|
||||
}}
|
||||
|
||||
signer, err := jose.NewSigner(key, opts)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Shouldn't be hard coded
|
||||
jwtBuilder := jwt.Signed(signer)
|
||||
jwtBuilder = jwtBuilder.Claims(jwt.Claims{
|
||||
Audience: []string{"dolthub-remote-api.liquidata.co"},
|
||||
Issuer: "dolt-client.liquidata.co",
|
||||
Subject: "doltClientCredentials/" + b32KIDStr,
|
||||
Audience: []string{c.Audience},
|
||||
Issuer: c.Issuer,
|
||||
Subject: c.Subject,
|
||||
Expiry: jwt.NewNumericDate(datetime.Now().Add(30 * time.Second)),
|
||||
})
|
||||
|
||||
return jwtBuilder.CompactSerialize()
|
||||
}
|
||||
|
||||
func (dc DoltCreds) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
|
||||
t, err := dc.toBearerToken()
|
||||
|
||||
func (c *RPCCreds) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
|
||||
t, err := c.toBearerToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return map[string]string{
|
||||
"authorization": "Bearer " + t,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (dc DoltCreds) RequireTransportSecurity() bool {
|
||||
return false
|
||||
func (c *RPCCreds) RequireTransportSecurity() bool {
|
||||
return c.RequireTLS
|
||||
}
|
||||
|
||||
const RemotesAPIAudience = "dolthub-remote-api.liquidata.co"
|
||||
const ClientIssuer = "dolt-client.liquidata.co"
|
||||
|
||||
func (dc DoltCreds) RPCCreds() *RPCCreds {
|
||||
b32KIDStr := dc.KeyIDBase32Str()
|
||||
return &RPCCreds{
|
||||
PrivKey: ed25519.PrivateKey(dc.PrivKey),
|
||||
KeyID: b32KIDStr,
|
||||
Audience: RemotesAPIAudience,
|
||||
Issuer: ClientIssuer,
|
||||
Subject: "doltClientCredentials/" + b32KIDStr,
|
||||
RequireTLS: false,
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -811,7 +811,7 @@ func (dEnv *DoltEnv) CredsDir() (string, error) {
|
||||
return getCredsDir(dEnv.hdp)
|
||||
}
|
||||
|
||||
func (dEnv *DoltEnv) UserRPCCreds() (creds.DoltCreds, bool, error) {
|
||||
func (dEnv *DoltEnv) UserDoltCreds() (creds.DoltCreds, bool, error) {
|
||||
kid, err := dEnv.Config.GetString(UserCreds)
|
||||
|
||||
if err == nil && kid != "" {
|
||||
@@ -826,7 +826,7 @@ func (dEnv *DoltEnv) UserRPCCreds() (creds.DoltCreds, bool, error) {
|
||||
return c, c.IsPrivKeyValid() && c.IsPubKeyValid(), err
|
||||
}
|
||||
|
||||
return creds.EmptyCreds, false, nil
|
||||
return creds.DoltCreds{}, false, nil
|
||||
}
|
||||
|
||||
// GetGRPCDialParams implements dbfactory.GRPCDialProvider
|
||||
|
||||
+2
-2
@@ -109,14 +109,14 @@ func (p GRPCDialProvider) getRPCCreds() (credentials.PerRPCCredentials, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
dCreds, valid, err := p.dEnv.UserRPCCreds()
|
||||
dCreds, valid, err := p.dEnv.UserDoltCreds()
|
||||
if err != nil {
|
||||
return nil, ErrInvalidCredsFile
|
||||
}
|
||||
if !valid {
|
||||
return nil, nil
|
||||
}
|
||||
return dCreds, nil
|
||||
return dCreds.RPCCreds(), nil
|
||||
}
|
||||
|
||||
// getUserAgentString returns a user agent string to use in GRPC requests.
|
||||
|
||||
@@ -59,6 +59,8 @@ type ServerArgs struct {
|
||||
ReadOnly bool
|
||||
Options []grpc.ServerOption
|
||||
|
||||
HttpInterceptor func(http.Handler) http.Handler
|
||||
|
||||
// If supplied, the listener(s) returned from Listeners() will be TLS
|
||||
// listeners. The scheme used in the URLs returned from the gRPC server
|
||||
// will be https.
|
||||
@@ -94,6 +96,9 @@ func NewServer(args ServerArgs) (*Server, error) {
|
||||
remotesapi.RegisterChunkStoreServiceServer(s.grpcSrv, chnkSt)
|
||||
|
||||
var handler http.Handler = newFileHandler(args.Logger, args.DBCache, args.FS, args.ReadOnly, sealer)
|
||||
if args.HttpInterceptor != nil {
|
||||
handler = args.HttpInterceptor(handler)
|
||||
}
|
||||
if args.HttpPort == args.GrpcPort {
|
||||
handler = grpcMultiplexHandler(s.grpcSrv, handler)
|
||||
} else {
|
||||
|
||||
@@ -16,6 +16,8 @@ package cluster
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
@@ -25,7 +27,9 @@ import (
|
||||
"github.com/dolthub/go-mysql-server/sql"
|
||||
"github.com/sirupsen/logrus"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
|
||||
"github.com/dolthub/dolt/go/libraries/doltcore/creds"
|
||||
"github.com/dolthub/dolt/go/libraries/doltcore/dbfactory"
|
||||
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
|
||||
"github.com/dolthub/dolt/go/libraries/doltcore/env"
|
||||
@@ -55,6 +59,7 @@ type Controller struct {
|
||||
sinterceptor serverinterceptor
|
||||
cinterceptor clientinterceptor
|
||||
lgr *logrus.Logger
|
||||
grpcCreds credentials.PerRPCCredentials
|
||||
|
||||
provider dbProvider
|
||||
iterSessions IterSessions
|
||||
@@ -193,7 +198,7 @@ func (c *Controller) applyCommitHooks(ctx context.Context, name string, bt *sql.
|
||||
}
|
||||
|
||||
func (c *Controller) gRPCDialProvider(denv *env.DoltEnv) dbfactory.GRPCDialProvider {
|
||||
return grpcDialProvider{env.NewGRPCDialProviderFromDoltEnv(denv), &c.cinterceptor, c.cfg}
|
||||
return grpcDialProvider{env.NewGRPCDialProviderFromDoltEnv(denv), &c.cinterceptor, c.cfg, c.grpcCreds}
|
||||
}
|
||||
|
||||
func (c *Controller) RegisterStoredProcedures(store procedurestore) {
|
||||
@@ -406,6 +411,25 @@ func (c *Controller) RemoteSrvServerArgs(ctx *sql.Context, args remotesrv.Server
|
||||
args.Options = c.ServerOptions()
|
||||
args = sqle.RemoteSrvServerArgs(ctx, args)
|
||||
args.DBCache = remotesrvStoreCache{args.DBCache, c}
|
||||
|
||||
pub, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
keyID := creds.PubKeyToKID(pub)
|
||||
keyIDStr := creds.B32CredsEncoding.EncodeToString(keyID)
|
||||
|
||||
args.HttpInterceptor = JWKSHandlerInterceptor(keyIDStr, pub)
|
||||
|
||||
c.grpcCreds = &creds.RPCCreds{
|
||||
PrivKey: priv,
|
||||
Audience: creds.RemotesAPIAudience,
|
||||
Issuer: creds.ClientIssuer,
|
||||
KeyID: keyIDStr,
|
||||
RequireTLS: false,
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/backoff"
|
||||
"google.golang.org/grpc/credentials"
|
||||
|
||||
"github.com/dolthub/dolt/go/libraries/doltcore/dbfactory"
|
||||
"github.com/dolthub/dolt/go/libraries/doltcore/grpcendpoint"
|
||||
@@ -34,9 +35,10 @@ import (
|
||||
// - client interceptors for transmitting our replication role.
|
||||
// - do not use environment credentials. (for now).
|
||||
type grpcDialProvider struct {
|
||||
orig dbfactory.GRPCDialProvider
|
||||
ci *clientinterceptor
|
||||
cfg Config
|
||||
orig dbfactory.GRPCDialProvider
|
||||
ci *clientinterceptor
|
||||
cfg Config
|
||||
creds credentials.PerRPCCredentials
|
||||
}
|
||||
|
||||
func (p grpcDialProvider) GetGRPCDialParams(config grpcendpoint.Config) (dbfactory.GRPCRemoteConfig, error) {
|
||||
@@ -45,6 +47,7 @@ func (p grpcDialProvider) GetGRPCDialParams(config grpcendpoint.Config) (dbfacto
|
||||
return dbfactory.GRPCRemoteConfig{}, err
|
||||
}
|
||||
config.TLSConfig = tlsConfig
|
||||
config.Creds = p.creds
|
||||
config.WithEnvCreds = false
|
||||
cfg, err := p.orig.GetGRPCDialParams(config)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright 2022 Dolthub, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
)
|
||||
|
||||
type JWKSHandler struct {
|
||||
KeyID string
|
||||
PublicKey ed25519.PublicKey
|
||||
}
|
||||
|
||||
func (h JWKSHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
b, err := json.Marshal(jose.JSONWebKeySet{
|
||||
Keys: []jose.JSONWebKey{
|
||||
jose.JSONWebKey{
|
||||
Key: h.PublicKey,
|
||||
KeyID: h.KeyID,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
http.Error(w, "error marshaling json", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Write(b)
|
||||
}
|
||||
|
||||
func JWKSHandlerInterceptor(keyID string, pub ed25519.PublicKey) func(http.Handler) http.Handler {
|
||||
jh := JWKSHandler{KeyID: keyID, PublicKey: pub}
|
||||
return func(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.EscapedPath() == "/.well-known/jwks.json" {
|
||||
jh.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
h.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
Creates files with a jwks and a jwt that can be validating using the jwks. Used in bats.
|
||||
@@ -24,8 +24,6 @@ import (
|
||||
|
||||
jose "gopkg.in/square/go-jose.v2"
|
||||
"gopkg.in/square/go-jose.v2/json"
|
||||
|
||||
"github.com/pquerna/cachecontrol"
|
||||
)
|
||||
|
||||
type cachedJWKS struct {
|
||||
@@ -39,12 +37,32 @@ func newCachedJWKS() *cachedJWKS {
|
||||
}
|
||||
|
||||
type fetchedJWKS struct {
|
||||
URL string
|
||||
cache *cachedJWKS
|
||||
URL string
|
||||
HTTPTransport *http.Transport
|
||||
cache *cachedJWKS
|
||||
}
|
||||
|
||||
func newJWKS(provider JWTProvider) *fetchedJWKS {
|
||||
return &fetchedJWKS{URL: provider.URL, cache: newCachedJWKS()}
|
||||
func newJWKS(provider JWTProvider) (*fetchedJWKS, error) {
|
||||
return newFetchedJWKS(provider.URL)
|
||||
}
|
||||
|
||||
func newFetchedJWKS(url string) (*fetchedJWKS, error) {
|
||||
ret := &fetchedJWKS{
|
||||
URL: url,
|
||||
cache: newCachedJWKS(),
|
||||
}
|
||||
|
||||
pwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Allows use of file:// for jwks location url for tests
|
||||
tr := &http.Transport{}
|
||||
tr.RegisterProtocol("file", http.NewFileTransport(http.Dir(pwd)))
|
||||
ret.HTTPTransport = tr
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (f *fetchedJWKS) needsRefresh() bool {
|
||||
@@ -55,14 +73,7 @@ func (f *fetchedJWKS) GetJWKS() (*jose.JSONWebKeySet, error) {
|
||||
f.cache.mutex.Lock()
|
||||
defer f.cache.mutex.Unlock()
|
||||
if f.needsRefresh() {
|
||||
tr := &http.Transport{}
|
||||
pwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Allows use of file:// for jwks location url for tests
|
||||
tr.RegisterProtocol("file", http.NewFileTransport(http.Dir(pwd)))
|
||||
client := &http.Client{Transport: tr}
|
||||
client := &http.Client{Transport: f.HTTPTransport}
|
||||
|
||||
request, err := http.NewRequest("GET", f.URL, nil)
|
||||
if err != nil {
|
||||
@@ -87,7 +98,6 @@ func (f *fetchedJWKS) GetJWKS() (*jose.JSONWebKeySet, error) {
|
||||
return nil, err
|
||||
}
|
||||
f.cache.value = &jwks
|
||||
_, _, err = cachecontrol.CachableResponse(request, response, cachecontrol.Options{})
|
||||
}
|
||||
}
|
||||
return f.cache.value, nil
|
||||
|
||||
@@ -29,9 +29,13 @@ type fetchingJWTValidator struct {
|
||||
expected jwt.Expected
|
||||
}
|
||||
|
||||
func NewJWTValidator(provider JWTProvider) JWTValidator {
|
||||
func NewJWTValidator(provider JWTProvider) (JWTValidator, error) {
|
||||
expected := jwt.Expected{Issuer: provider.Issuer, Audience: jwt.Audience{provider.Audience}}
|
||||
return &fetchingJWTValidator{jwks: newJWKS(provider), expected: expected}
|
||||
jwks, err := newJWKS(provider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &fetchingJWTValidator{jwks: jwks, expected: expected}, nil
|
||||
}
|
||||
|
||||
func (v *fetchingJWTValidator) ValidateJWT(unparsed string, reqTime time.Time) (*Claims, error) {
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
#!/usr/bin/env bats
|
||||
load $BATS_TEST_DIRNAME/helper/common.bash
|
||||
load $BATS_TEST_DIRNAME/helper/query-server-common.bash
|
||||
|
||||
make_repo() {
|
||||
mkdir "$1"
|
||||
cd "$1"
|
||||
dolt init
|
||||
cd ..
|
||||
}
|
||||
|
||||
setup() {
|
||||
skiponwindows "Missing dependencies"
|
||||
setup_no_dolt_init
|
||||
make_repo repo1
|
||||
(cd "$BATS_TEST_DIRNAME"/../../go/libraries/utils/jwtauth/gen_keys && go run .)
|
||||
}
|
||||
|
||||
teardown() {
|
||||
stop_sql_server
|
||||
teardown_common
|
||||
rm -rf "$BATS_TEST_DIRNAME/../../go/libraries/utils/jwtauth/gen_keys/token.jwt"
|
||||
rm -rf "$BATS_TEST_DIRNAME/../../go/libraries/utils/jwtauth/gen_keys/test_jwks.json"
|
||||
}
|
||||
|
||||
|
||||
@test "sql-jwt-auth: jwt auth from config" {
|
||||
cd repo1
|
||||
cp "$BATS_TEST_DIRNAME"/../../go/cmd/dolt/commands/sqlserver/testdata/chain_key.pem .
|
||||
cp "$BATS_TEST_DIRNAME"/../../go/cmd/dolt/commands/sqlserver/testdata/chain_cert.pem .
|
||||
cp "$BATS_TEST_DIRNAME"/../../go/libraries/utils/jwtauth/gen_keys/test_jwks.json .
|
||||
PORT=$( definePORT )
|
||||
TOKEN="`cat $BATS_TEST_DIRNAME/../../go/libraries/utils/jwtauth/gen_keys/token.jwt`"
|
||||
|
||||
cat >config.yml <<EOF
|
||||
log_level: debug
|
||||
user:
|
||||
name: dolt
|
||||
listener:
|
||||
host: "0.0.0.0"
|
||||
port: $PORT
|
||||
tls_cert: chain_cert.pem
|
||||
tls_key: chain_key.pem
|
||||
require_secure_transport: true
|
||||
privilege_file: privs.json
|
||||
jwks:
|
||||
- name: jwksname
|
||||
location_url: file:///test_jwks.json
|
||||
claims:
|
||||
alg: RS256
|
||||
aud: my_resource
|
||||
sub: test_jwt_user
|
||||
iss: dolthub.com
|
||||
fields_to_log: [on_behalf_of,id]
|
||||
EOF
|
||||
|
||||
dolt sql --privilege-file=privs.json -q "CREATE USER dolt@'127.0.0.1'"
|
||||
dolt sql --privilege-file=privs.json -q "GRANT ALL ON *.* TO dolt@'127.0.0.1' WITH GRANT OPTION"
|
||||
dolt sql --privilege-file=privs.json -q "CREATE USER test_jwt_user@'127.0.0.1' IDENTIFIED WITH authentication_dolt_jwt AS 'jwks=jwksname,sub=test_jwt_user,iss=dolthub.com,aud=my_resource'"
|
||||
dolt sql --privilege-file=privs.json -q "GRANT ALL ON *.* TO test_jwt_user@'127.0.0.1' WITH GRANT OPTION"
|
||||
|
||||
dolt sql-server --config ./config.yml &
|
||||
SERVER_PID=$!
|
||||
|
||||
|
||||
# We do things manually here because we need TLS support.
|
||||
python3 -c '
|
||||
import mysql.connector
|
||||
import sys
|
||||
import time
|
||||
import os
|
||||
i=0
|
||||
|
||||
os.environ["LIBMYSQL_ENABLE_CLEARTEXT_PLUGIN"] = "1"
|
||||
args = sys.argv[sys.argv.index("--") + 1:]
|
||||
password = args[0]
|
||||
|
||||
while True:
|
||||
try:
|
||||
with mysql.connector.connect(
|
||||
host="127.0.0.1",
|
||||
user="test_jwt_user",
|
||||
password=password,
|
||||
port='"$PORT"',
|
||||
database="repo1",
|
||||
connection_timeout=1
|
||||
) as c:
|
||||
cursor = c.cursor()
|
||||
cursor.execute("show tables")
|
||||
for (t) in cursor:
|
||||
print(t)
|
||||
sys.exit(0)
|
||||
except mysql.connector.Error as err:
|
||||
if err.errno != 2003:
|
||||
raise err
|
||||
else:
|
||||
i += 1
|
||||
time.sleep(1)
|
||||
if i == 10:
|
||||
raise err
|
||||
' -- "$TOKEN"
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -258,11 +259,27 @@ func (s *SqlServer) Restart(newargs *[]string) error {
|
||||
return s.Cmd.Start()
|
||||
}
|
||||
|
||||
func (s *SqlServer) DB() (*sql.DB, error) {
|
||||
func (s *SqlServer) DB(c Connection) (*sql.DB, error) {
|
||||
authority := "root"
|
||||
if c.User != "" {
|
||||
authority = c.User
|
||||
}
|
||||
pass, err := c.Password()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if pass != "" {
|
||||
authority += ":" + pass
|
||||
}
|
||||
location := fmt.Sprintf("tcp(127.0.0.1:%d)", s.Port)
|
||||
dbname := s.DBName
|
||||
dsn := fmt.Sprintf("%s@%s/%s?allowAllFiles=true&tls=preferred", authority, location, dbname)
|
||||
params := make(url.Values)
|
||||
params.Set("allowAllFiles", "true")
|
||||
params.Set("tls", "preferred")
|
||||
for k, v := range c.DriverParams {
|
||||
params.Set(k, v)
|
||||
}
|
||||
dsn := fmt.Sprintf("%s@%s/%s?%s", authority, location, dbname, params.Encode())
|
||||
db, err := sql.Open("mysql", dsn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
Creates files with a jwks and a jwt that can be validated using the jwks.
|
||||
|
||||
Used in sql-server-jwt-auth.yaml.
|
||||
@@ -0,0 +1,14 @@
|
||||
module github.com/dolthub/dolt/integration-tests/go-sql-server-driver/genjwt
|
||||
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.3.0
|
||||
gopkg.in/square/go-jose.v2 v2.6.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/stretchr/testify v1.8.1 // indirect
|
||||
golang.org/x/crypto v0.1.0 // indirect
|
||||
)
|
||||
@@ -0,0 +1,24 @@
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
|
||||
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
|
||||
gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
+6
-3
@@ -21,6 +21,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -28,6 +29,8 @@ import (
|
||||
"gopkg.in/square/go-jose.v2/jwt"
|
||||
)
|
||||
|
||||
const RelPath = "../testdata"
|
||||
|
||||
var sub = "test_user"
|
||||
var iss = "dolthub.com"
|
||||
var aud = "my_resource"
|
||||
@@ -55,7 +58,7 @@ func main() {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile("token.jwt", []byte(jwt), 0644)
|
||||
err = ioutil.WriteFile(filepath.Join(RelPath, "token.jwt"), []byte(jwt), 0644)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
@@ -75,7 +78,7 @@ func writeJWKSToFile(pubKey crypto.PublicKey, kid string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile("test_jwks.json", jwksjson, 0644)
|
||||
err = ioutil.WriteFile(filepath.Join(RelPath, "test_jwks.json"), jwksjson, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -96,7 +99,7 @@ func generateJWT(privKey *rsa.PrivateKey, kid string) (string, error) {
|
||||
Issuer: iss,
|
||||
Subject: sub,
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
Expiry: jwt.NewNumericDate(now.Add(30 * time.Second)),
|
||||
Expiry: jwt.NewNumericDate(now.Add(364 * 24 * time.Hour)),
|
||||
}
|
||||
privClaims := struct {
|
||||
OnBehalfOf string `json:"on_behalf_of"`
|
||||
@@ -22,6 +22,10 @@ func TestConfig(t *testing.T) {
|
||||
RunTestsFile(t, "tests/sql-server-config.yaml")
|
||||
}
|
||||
|
||||
func TestJWTAuth(t *testing.T) {
|
||||
RunTestsFile(t, "tests/sql-server-jwt-auth.yaml")
|
||||
}
|
||||
|
||||
func TestCluster(t *testing.T) {
|
||||
RunTestsFile(t, "tests/sql-server-cluster.yaml")
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"keys":[{"use":"sig","kty":"RSA","kid":"56321c6d-cdb9-4cb2-89f1-3c7cf6bb051d","alg":"RS256","n":"oKNBTaaBljGkmvns_lk4y8HWhquNFUXLcNofg56vgvmareivMABSq0miNg1G3Bgew6EGemTGSFNBCicXH1gGZ7tBnpxDj8Vpt5nYXEeywmiBKWxbVu8o9XamuLFXPOhK_BzRPKnoX-J-BoopxWNKUWwJfCM-69MgRXl-K5w4gjGrHePXtpDylT-zWMNCAvHDGd8kwVj0EKeI2-PjxBRzltCfEWPblLcif4pirBzsCho9fC133XlY3ixT162YYqt8M7Grr0zxRca8OhuVISrkJxh7p6M0eHpfUrAjuCGCqcIs0WLMZ-aZNzApbFebx5OeTBpe1Xi6oJxjuKozTy_W0w","e":"AQAB"}]}
|
||||
@@ -0,0 +1 @@
|
||||
eyJhbGciOiJSUzI1NiIsImtpZCI6IjU2MzIxYzZkLWNkYjktNGNiMi04OWYxLTNjN2NmNmJiMDUxZCIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsibXlfcmVzb3VyY2UiXSwiZXhwIjoxNjk4Nzk0MTI0LCJpYXQiOjE2NjczNDQ1MjQsImlzcyI6ImRvbHRodWIuY29tIiwianRpIjoiMjc3YmIyNmMtMGMwOC00MjA3LWFiYjAtYzIyMGJmZTJlMDc2Iiwib25fYmVoYWxmX29mIjoibXlfdXNlciIsInN1YiI6InRlc3RfdXNlciJ9.OeKKNLQeQ6JWqFeu_lP9zGU1yCKvx5Xo868S82kEjJ_CGSj_K3Y3GG2rht8977SdULivRwxxtOM2LH7VJ0WkgmjCzERsA40z_SNwBwiWnIUzmT3uXsBsq31xgwG9xO-LrwMOJ-66Y1UgFjsmQmjV4Bw5vTkuGOkp87El-8MeRIC7eWzWotmcdSSWTCtFJumomHnDrTyYtvL0bLaqMCkUApdgAjB9At9q7a75kJ3kklTFmVJs9sO9cN3hsWTLmV-mM3PO6OKmNQbRqz92qTrTFIWLc92ooOVKrb6v5yY5GvH0z1bBpgNdSBbImS0FsrLhzBBDAJPK8uNoNuaLYVcvLQ
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -63,6 +64,25 @@ type Connection struct {
|
||||
// connection based on things that are happening, such as cluster role
|
||||
// transitions.
|
||||
RetryAttempts int `yaml:"retry_attempts"`
|
||||
|
||||
// The user to connect as.
|
||||
User string `yaml:"user"`
|
||||
// The password to connect with.
|
||||
Pass string `yaml:"password"`
|
||||
PassFile string `yaml:"password_file"`
|
||||
// Any driver params to pass in the DSN.
|
||||
DriverParams map[string]string `yaml:"driver_params"`
|
||||
}
|
||||
|
||||
func (c Connection) Password() (string, error) {
|
||||
if c.PassFile != "" {
|
||||
bs, err := os.ReadFile(c.PassFile)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(string(bs)), nil
|
||||
}
|
||||
return c.Pass, nil
|
||||
}
|
||||
|
||||
// |RestartArgs| are possible arguments, to change the arguments which are
|
||||
@@ -329,7 +349,7 @@ func (test Test) Run(t *testing.T) {
|
||||
require.NotNilf(t, server, "error in test spec: could not find server %s for connection %d", c.On, i)
|
||||
if c.RetryAttempts > 1 {
|
||||
RetryTestRun(t, c.RetryAttempts, func(t require.TestingT) {
|
||||
db, err := server.DB()
|
||||
db, err := server.DB(c)
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
@@ -343,7 +363,7 @@ func (test Test) Run(t *testing.T) {
|
||||
})
|
||||
} else {
|
||||
func() {
|
||||
db, err := server.DB()
|
||||
db, err := server.DB(c)
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
tests:
|
||||
- name: jwt auth from config
|
||||
repos:
|
||||
- name: repo1
|
||||
with_files:
|
||||
- name: chain_key.pem
|
||||
source_path: testdata/rsa_key.pem
|
||||
- name: chain_cert.pem
|
||||
source_path: testdata/rsa_chain.pem
|
||||
- name: test_jwks.json
|
||||
source_path: testdata/test_jwks.json
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
listener:
|
||||
tls_key: chain_key.pem
|
||||
tls_cert: chain_cert.pem
|
||||
require_secure_transport: true
|
||||
jwks:
|
||||
- name: jwksname
|
||||
location_url: file:///test_jwks.json
|
||||
claims:
|
||||
alg: RS256
|
||||
aud: my_resource
|
||||
sub: test_jwt_user
|
||||
iss: dolthub.com
|
||||
fields_to_log: [on_behalf_of,id]
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
connections:
|
||||
- on: repo1
|
||||
queries:
|
||||
- exec: "CREATE USER test_jwt_user@'127.0.0.1' IDENTIFIED WITH authentication_dolt_jwt AS 'jwks=jwksname,sub=test_jwt_user,iss=dolthub.com,aud=my_resource'"
|
||||
- exec: "GRANT ALL ON *.* TO test_jwt_user@'127.0.0.1' WITH GRANT OPTION"
|
||||
- on: repo1
|
||||
user: test_jwt_user
|
||||
password_file: testdata/token.jwt
|
||||
driver_params:
|
||||
allowCleartextPasswords: "true"
|
||||
queries:
|
||||
- query: "select 2+2 from dual"
|
||||
result:
|
||||
columns: ["2+2"]
|
||||
rows: [["4"]]
|
||||
Reference in New Issue
Block a user