Skip to content

Commit 226f07b

Browse files
committed
v0.4.0
1 parent 21097ea commit 226f07b

33 files changed

+2977
-1919
lines changed

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@
1010
<img src="assets/gui.webp" alt="Warthog UI" title="Warthog cross platform gRPC client"
1111
</p>
1212

13+
1314
## Features
1415

1516
- Automatic parsing of proto definitions to render services and input messages
1617
- `.proto` file discovery
1718
- Selection of multiple services and methods
1819
- Configuration of TLS, including disabling TLS (plain text)
20+
- Authentication: Basic, Bearer Token, JWT, GCE
1921
- Input generation for all scalar types
2022
- Input generation for nested and looped messages
2123
- Input generation for enums, including nested

adapter/grpc/auth.go

+120
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package grpc
2+
3+
import (
4+
"context"
5+
"encoding/base64"
6+
"fmt"
7+
8+
"github.com/dgrijalva/jwt-go"
9+
"golang.org/x/oauth2"
10+
"google.golang.org/grpc"
11+
"google.golang.org/grpc/credentials/oauth"
12+
13+
"github.com/forest33/warthog/business/entity"
14+
)
15+
16+
type basicAuth struct {
17+
login string
18+
password string
19+
}
20+
21+
var symmetricAlgorithms = map[string]struct{}{
22+
"HS256": {},
23+
"HS384": {},
24+
"HS512": {},
25+
}
26+
27+
func (c *Client) getAuth(auth *entity.Auth) (grpc.DialOption, error) {
28+
if auth == nil || auth.Type == entity.AuthTypeNone {
29+
return nil, nil
30+
}
31+
32+
switch auth.Type {
33+
case entity.AuthTypeBasic:
34+
return c.authBasic(auth)
35+
case entity.AuthTypeBearer:
36+
return c.authBearer(auth)
37+
case entity.AuthTypeJWT:
38+
return c.authJWT(auth)
39+
case entity.AuthTypeGCE:
40+
return c.authGCE(auth)
41+
}
42+
43+
return nil, nil
44+
}
45+
46+
func (c *Client) authBasic(auth *entity.Auth) (grpc.DialOption, error) {
47+
return grpc.WithPerRPCCredentials(basicAuth{
48+
login: auth.Login,
49+
password: auth.Password,
50+
}), nil
51+
}
52+
53+
func (c *Client) authBearer(auth *entity.Auth) (grpc.DialOption, error) {
54+
token := &oauth2.Token{
55+
AccessToken: auth.Token,
56+
}
57+
token.TokenType = auth.HeaderPrefix
58+
59+
return grpc.WithPerRPCCredentials(
60+
oauth.NewOauthAccess(token),
61+
), nil
62+
}
63+
64+
func (c *Client) authJWT(auth *entity.Auth) (grpc.DialOption, error) {
65+
signingMethod := jwt.GetSigningMethod(auth.Algorithm)
66+
if signingMethod == nil {
67+
return nil, fmt.Errorf("unknown signing algorithm: %s", auth.Algorithm)
68+
}
69+
70+
var (
71+
secret interface{}
72+
err error
73+
)
74+
75+
if _, ok := symmetricAlgorithms[auth.Algorithm]; ok {
76+
secret = []byte(auth.Secret)
77+
if auth.SecretBase64 {
78+
secret, err = base64.StdEncoding.DecodeString(auth.Secret)
79+
if err != nil {
80+
return nil, err
81+
}
82+
}
83+
} else {
84+
secret, err = jwt.ParseRSAPrivateKeyFromPEM([]byte(auth.PrivateKey))
85+
if err != nil {
86+
return nil, err
87+
}
88+
}
89+
90+
jwtToken := jwt.NewWithClaims(signingMethod, jwt.MapClaims(auth.Payload))
91+
token, err := jwtToken.SignedString(secret)
92+
if err != nil {
93+
return nil, err
94+
}
95+
96+
return c.authBearer(&entity.Auth{
97+
Token: token,
98+
HeaderPrefix: auth.HeaderPrefix,
99+
})
100+
}
101+
102+
func (c *Client) authGCE(auth *entity.Auth) (grpc.DialOption, error) {
103+
perRPC, err := oauth.NewServiceAccountFromKey([]byte(auth.GoogleToken), auth.GoogleScopes...)
104+
if err != nil {
105+
return nil, err
106+
}
107+
return grpc.WithPerRPCCredentials(perRPC), nil
108+
}
109+
110+
func (b basicAuth) GetRequestMetadata(ctx context.Context, _ ...string) (map[string]string, error) {
111+
auth := b.login + ":" + b.password
112+
enc := base64.StdEncoding.EncodeToString([]byte(auth))
113+
return map[string]string{
114+
"authorization": "Basic " + enc,
115+
}, nil
116+
}
117+
118+
func (b basicAuth) RequireTransportSecurity() bool {
119+
return true
120+
}

adapter/grpc/client.go

+15-7
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ func (c *Client) SetSettings(cfg *entity.Settings) {
6161
}
6262

6363
// Connect connecting to gRPC server
64-
func (c *Client) Connect(addr string, opts ...ClientOpt) error {
64+
func (c *Client) Connect(addr string, auth *entity.Auth, opts ...ClientOpt) error {
6565
if defaultOptions != nil {
6666
c.opts = *defaultOptions
6767
}
@@ -78,6 +78,12 @@ func (c *Client) Connect(addr string, opts ...ClientOpt) error {
7878
return err
7979
}
8080

81+
if opt, err := c.getAuth(auth); err != nil {
82+
return err
83+
} else if opt != nil {
84+
dialOptions = append(dialOptions, opt)
85+
}
86+
8187
ctx := c.ctx
8288
if !*c.cfg.NonBlockingConnection {
8389
var cancel context.CancelFunc
@@ -115,17 +121,19 @@ func (c *Client) loadTLSCredentials() (credentials.TransportCredentials, error)
115121
return nil, fmt.Errorf("failed to add server CA's certificate")
116122
}
117123

118-
clientCert, err := tls.X509KeyPair([]byte(c.opts.clientCertificate), []byte(c.opts.clientKey))
119-
if err != nil {
120-
return nil, err
121-
}
122-
123124
cfg := &tls.Config{
124-
Certificates: []tls.Certificate{clientCert},
125125
RootCAs: pool,
126126
InsecureSkipVerify: c.opts.insecureSkipVerify,
127127
}
128128

129+
if c.opts.clientCertificate != "" && c.opts.clientKey != "" {
130+
certificates, err := tls.X509KeyPair([]byte(c.opts.clientCertificate), []byte(c.opts.clientKey))
131+
if err != nil {
132+
return nil, err
133+
}
134+
cfg.Certificates = []tls.Certificate{certificates}
135+
}
136+
129137
return credentials.NewTLS(cfg), nil
130138
}
131139

bin/version

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.3.16
1+
0.4.0

business/entity/auth.go

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package entity
2+
3+
const (
4+
AuthTypeNone = "none"
5+
AuthTypeBasic = "basic"
6+
AuthTypeBearer = "bearer"
7+
AuthTypeJWT = "jwt"
8+
AuthTypeGCE = "google"
9+
)

business/entity/query.go

+7
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ type Query struct {
3737
Method string
3838
Data map[string]interface{}
3939
Metadata []string
40+
Auth *Auth
4041
}
4142

4243
// QueryResponse gRPC response
@@ -75,6 +76,12 @@ func (r *Query) Model(server map[string]interface{}) error {
7576
r.Metadata = append(r.Metadata, k, v.(string))
7677
}
7778
}
79+
if v, ok := server["auth"]; ok {
80+
r.Auth = &Auth{}
81+
if err := r.Auth.Model(v.(map[string]interface{})); err != nil {
82+
return err
83+
}
84+
}
7885

7986
return nil
8087
}

business/entity/workspace.server.go

+75
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
package entity
33

44
import (
5+
"encoding/json"
56
"errors"
7+
"strings"
68

79
"github.com/forest33/warthog/pkg/structs"
810
)
@@ -42,6 +44,23 @@ type WorkspaceItemServer struct {
4244
ClientCertificate string `json:"client_certificate,omitempty"`
4345
ClientKey string `json:"client_key,omitempty"`
4446
Request map[string]map[string]*SavedQuery `json:"request"`
47+
Auth *Auth `json:"auth"`
48+
}
49+
50+
// Auth authentication data
51+
type Auth struct {
52+
Type string `json:"type,omitempty"`
53+
Login string `json:"login,omitempty"`
54+
Password string `json:"password,omitempty"`
55+
Token string `json:"token,omitempty"`
56+
Algorithm string `json:"algorithm,omitempty"`
57+
Secret string `json:"secret,omitempty"`
58+
PrivateKey string `json:"private_key,omitempty"`
59+
SecretBase64 bool `json:"secret_base64,omitempty"`
60+
HeaderPrefix string `json:"header_prefix,omitempty"`
61+
Payload map[string]interface{} `json:"payload,omitempty"`
62+
GoogleScopes []string `json:"google_scopes,omitempty"`
63+
GoogleToken string `json:"google_token,omitempty"`
4564
}
4665

4766
// Model creates ServerRequest from UI request
@@ -122,6 +141,62 @@ func (s *WorkspaceItemServer) Model(server map[string]interface{}) error {
122141
if v, ok := server["client_key"]; ok && v != nil {
123142
s.ClientKey = v.(string)
124143
}
144+
if v, ok := server["auth"]; ok && v != nil {
145+
s.Auth = &Auth{}
146+
if err := s.Auth.Model(v.(map[string]interface{})); err != nil {
147+
return err
148+
}
149+
}
150+
151+
return nil
152+
}
153+
154+
// Model creates Auth from UI request
155+
func (s *Auth) Model(auth map[string]interface{}) error {
156+
authType, ok := auth["type"]
157+
if !ok || authType == AuthTypeNone {
158+
s.Type = AuthTypeNone
159+
return nil
160+
}
161+
162+
s.Type = authType.(string)
163+
164+
if v, ok := auth["login"]; ok {
165+
s.Login = v.(string)
166+
}
167+
if v, ok := auth["password"]; ok {
168+
s.Password = v.(string)
169+
}
170+
if v, ok := auth["token"]; ok {
171+
s.Token = v.(string)
172+
}
173+
if v, ok := auth["algorithm"]; ok {
174+
s.Algorithm = v.(string)
175+
}
176+
if v, ok := auth["secret"]; ok {
177+
s.Secret = v.(string)
178+
}
179+
if v, ok := auth["private_key"]; ok {
180+
s.PrivateKey = v.(string)
181+
}
182+
if v, ok := auth["secret_base64"]; ok {
183+
s.SecretBase64 = v.(bool)
184+
}
185+
if v, ok := auth["header_prefix"]; ok {
186+
s.HeaderPrefix = strings.TrimSpace(v.(string))
187+
}
188+
if v, ok := auth["payload"]; ok {
189+
s.Payload = map[string]interface{}{}
190+
if err := json.Unmarshal([]byte(v.(string)), &s.Payload); err != nil {
191+
return err
192+
}
193+
}
194+
if v, ok := auth["google_token"]; ok {
195+
s.GoogleToken = v.(string)
196+
}
197+
if v, ok := auth["google_scopes"]; ok {
198+
s.GoogleScopes = strings.Split(v.(string), ",")
199+
}
125200

126201
return nil
127202
}

0 commit comments

Comments
 (0)