Skip to content

Commit 7e1cf6a

Browse files
graphql: add GraphQL authorisation (#5179)
* add Auth directive (#5178) * parse auth rules (#5180) * added query rewriting and e2e tests (#5229) * parse and evaluate RBAC rules. (#5210) * added test cases for auth schema parsing. (#5195) * process auth query rules (#5181) * send auth variable in custom jwt token. (#5220) * auth on get and mutation results (#5259) * delete authorization (#5270) * parse auth meta info from schema. (#5269) * auth on add update mutations (#5300) * query e2e tests for authentication (#5312) * more testing around additional deletes and auth (#5357) * add RSA algo for JWT token verification. (#5358)
1 parent 0024241 commit 7e1cf6a

File tree

70 files changed

+5846
-247
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

70 files changed

+5846
-247
lines changed

graphql/admin/schema.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ func (asr *updateSchemaResolver) Execute(
106106
return &dgoapi.Response{Json: b}, err
107107
}
108108

109+
req.CommitNow = true
109110
resp, err := asr.baseMutationExecutor.Execute(ctx, req)
110111
if err != nil {
111112
return nil, err
@@ -124,6 +125,10 @@ func (asr *updateSchemaResolver) Execute(
124125
return resp, nil
125126
}
126127

128+
func (asr *updateSchemaResolver) CommitOrAbort(ctx context.Context, tc *dgoapi.TxnContext) error {
129+
return asr.baseMutationExecutor.CommitOrAbort(ctx, tc)
130+
}
131+
127132
func (gsr *getSchemaResolver) Rewrite(ctx context.Context,
128133
gqlQuery schema.Query) (*gql.GraphQuery, error) {
129134
gsr.gqlQuery = gqlQuery
@@ -138,6 +143,10 @@ func (gsr *getSchemaResolver) Execute(
138143
return &dgoapi.Response{Json: b}, err
139144
}
140145

146+
func (gsr *getSchemaResolver) CommitOrAbort(ctx context.Context, tc *dgoapi.TxnContext) error {
147+
return nil
148+
}
149+
141150
func doQuery(gql *gqlSchema, field schema.Field) ([]byte, error) {
142151

143152
var buf bytes.Buffer

graphql/admin/update_group.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func (urw *updateGroupRewriter) Rewrite(
3434
return nil, nil
3535
}
3636

37-
upsertQuery := resolve.RewriteUpsertQueryFromMutation(m)
37+
upsertQuery := resolve.RewriteUpsertQueryFromMutation(m, nil)
3838
srcUID := resolve.MutationQueryVarUID
3939

4040
var errSet, errDel error

graphql/authorization/auth.go

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
/*
2+
* Copyright 2020 Dgraph Labs, Inc. and Contributors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package authorization
18+
19+
import (
20+
"bytes"
21+
"context"
22+
"crypto/rsa"
23+
"encoding/json"
24+
"fmt"
25+
"net/http"
26+
"regexp"
27+
"strings"
28+
"time"
29+
30+
"github.com/dgrijalva/jwt-go"
31+
"github.com/pkg/errors"
32+
"google.golang.org/grpc/metadata"
33+
)
34+
35+
type ctxKey string
36+
37+
const (
38+
AuthJwtCtxKey = ctxKey("authorizationJwt")
39+
RSA256 = "RS256"
40+
HMAC256 = "HS256"
41+
)
42+
43+
var (
44+
metainfo = &AuthMeta{}
45+
)
46+
47+
type AuthMeta struct {
48+
HMACPublicKey string
49+
RSAPublicKey *rsa.PublicKey
50+
Header string
51+
Namespace string
52+
Algo string
53+
}
54+
55+
func (m *AuthMeta) Parse(schema string) error {
56+
lastCommentIdx := strings.LastIndex(schema, "#")
57+
if lastCommentIdx == -1 {
58+
return nil
59+
}
60+
lastComment := schema[lastCommentIdx:]
61+
if !strings.HasPrefix(lastComment, "# Authorization") {
62+
return nil
63+
}
64+
65+
// This regex matches authorization information present in the last line of the schema.
66+
// Format: # Authorization <HTTP header> <Claim namespace> <Algorithm> "<verification key>"
67+
// Example: # Authorization X-Test-Auth https://xyz.io/jwt/claims HS256 "secretkey"
68+
// On successful regex match the index for the following strings will be returned.
69+
// [0][0]:[0][1] : # Authorization X-Test-Auth https://xyz.io/jwt/claims HS256 "secretkey"
70+
// [0][2]:[0][3] : Authorization, [0][4]:[0][5] : X-Test-Auth,
71+
// [0][6]:[0][7] : https://xyz.io/jwt/claims,
72+
// [0][8]:[0][9] : HS256, [0][10]:[0][11] : secretkey
73+
authMetaRegex, err :=
74+
regexp.Compile(`^#[\s]([^\s]+)[\s]+([^\s]+)[\s]+([^\s]+)[\s]+([^\s]+)[\s]+"([^\"]+)"`)
75+
if err != nil {
76+
return errors.Errorf("error while parsing jwt authorization info: %v", err)
77+
}
78+
idx := authMetaRegex.FindAllStringSubmatchIndex(lastComment, -1)
79+
if len(idx) != 1 || len(idx[0]) != 12 ||
80+
!strings.HasPrefix(lastComment, lastComment[idx[0][0]:idx[0][1]]) {
81+
return errors.Errorf("error while parsing jwt authorization info")
82+
}
83+
84+
m.Header = lastComment[idx[0][4]:idx[0][5]]
85+
m.Namespace = lastComment[idx[0][6]:idx[0][7]]
86+
m.Algo = lastComment[idx[0][8]:idx[0][9]]
87+
88+
key := lastComment[idx[0][10]:idx[0][11]]
89+
if m.Algo == HMAC256 {
90+
m.HMACPublicKey = key
91+
return nil
92+
}
93+
if m.Algo != RSA256 {
94+
return errors.Errorf(
95+
"invalid jwt algorithm: found %s, but supported options are HS256 or RS256", m.Algo)
96+
}
97+
98+
// The jwt library internally uses `bytes.IndexByte(data, '\n')` to fetch new line and fails
99+
// if we have newline "\n" as ASCII value {92,110} instead of the actual ASCII value of 10.
100+
// To fix this we replace "\n" with new line's ASCII value.
101+
bytekey := bytes.ReplaceAll([]byte(key), []byte{92, 110}, []byte{10})
102+
103+
m.RSAPublicKey, err = jwt.ParseRSAPublicKeyFromPEM(bytekey)
104+
return err
105+
}
106+
107+
func ParseAuthMeta(schema string) error {
108+
return metainfo.Parse(schema)
109+
}
110+
111+
// AttachAuthorizationJwt adds any incoming JWT authorization data into the grpc context metadata.
112+
func AttachAuthorizationJwt(ctx context.Context, r *http.Request) context.Context {
113+
authorizationJwt := r.Header.Get(metainfo.Header)
114+
if authorizationJwt == "" {
115+
return ctx
116+
}
117+
118+
md, ok := metadata.FromIncomingContext(ctx)
119+
if !ok {
120+
md = metadata.New(nil)
121+
}
122+
123+
md.Append(string(AuthJwtCtxKey), authorizationJwt)
124+
ctx = metadata.NewIncomingContext(ctx, md)
125+
return ctx
126+
}
127+
128+
type CustomClaims struct {
129+
AuthVariables map[string]interface{}
130+
jwt.StandardClaims
131+
}
132+
133+
func (c *CustomClaims) UnmarshalJSON(data []byte) error {
134+
// Unmarshal the standard claims first.
135+
if err := json.Unmarshal(data, &c.StandardClaims); err != nil {
136+
return err
137+
}
138+
139+
var result map[string]interface{}
140+
if err := json.Unmarshal(data, &result); err != nil {
141+
return err
142+
}
143+
144+
// Unmarshal the auth variables for a particular namespace.
145+
if authVariables, ok := result[metainfo.Namespace]; ok {
146+
c.AuthVariables, _ = authVariables.(map[string]interface{})
147+
}
148+
return nil
149+
}
150+
151+
func ExtractAuthVariables(ctx context.Context) (map[string]interface{}, error) {
152+
// Extract the jwt and unmarshal the jwt to get the auth variables.
153+
md, ok := metadata.FromIncomingContext(ctx)
154+
if !ok {
155+
return nil, nil
156+
}
157+
158+
jwtToken := md.Get(string(AuthJwtCtxKey))
159+
if len(jwtToken) == 0 {
160+
return nil, nil
161+
} else if len(jwtToken) > 1 {
162+
return nil, fmt.Errorf("invalid jwt auth token")
163+
}
164+
165+
return validateToken(jwtToken[0])
166+
}
167+
168+
func validateToken(jwtStr string) (map[string]interface{}, error) {
169+
if metainfo.Algo == "" {
170+
return nil, fmt.Errorf(
171+
"jwt token cannot be validated because verification algorithm is not set")
172+
}
173+
174+
token, err :=
175+
jwt.ParseWithClaims(jwtStr, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
176+
algo, _ := token.Header["alg"].(string)
177+
if algo != metainfo.Algo {
178+
return nil, errors.Errorf("unexpected signing method: Expected %s Found %s",
179+
metainfo.Algo, algo)
180+
}
181+
if algo == HMAC256 {
182+
if _, ok := token.Method.(*jwt.SigningMethodHMAC); ok {
183+
return []byte(metainfo.HMACPublicKey), nil
184+
}
185+
} else if algo == RSA256 {
186+
if _, ok := token.Method.(*jwt.SigningMethodRSA); ok {
187+
return metainfo.RSAPublicKey, nil
188+
}
189+
}
190+
return nil, errors.Errorf("couldn't parse signing method from token header: %s", algo)
191+
})
192+
193+
if err != nil {
194+
return nil, errors.Errorf("unable to parse jwt token:%v", err)
195+
}
196+
197+
claims, ok := token.Claims.(*CustomClaims)
198+
if !ok || !token.Valid {
199+
return nil, errors.Errorf("claims in jwt token is not map claims")
200+
}
201+
202+
// by default, the MapClaims.Valid will return true if the exp field is not set
203+
// here we enforce the checking to make sure that the refresh token has not expired
204+
now := time.Now().Unix()
205+
if !claims.VerifyExpiresAt(now, true) {
206+
return nil, errors.Errorf("Token is expired") // the same error msg that's used inside jwt-go
207+
}
208+
209+
return claims.AuthVariables, nil
210+
}

graphql/dgraph/execute.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,9 @@ func (dg *DgraphEx) Execute(ctx context.Context, req *dgoapi.Request) (*dgoapi.R
5757

5858
return resp, schema.GQLWrapf(err, "Dgraph execution failed")
5959
}
60+
61+
// CommitOrAbort is the underlying dgraph implementation for commiting a Dgraph transaction
62+
func (dg *DgraphEx) CommitOrAbort(ctx context.Context, tc *dgoapi.TxnContext) error {
63+
_, err := (&edgraph.Server{}).CommitOrAbort(ctx, tc)
64+
return err
65+
}

graphql/dgraph/graphquery.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,13 @@ func AsString(query *gql.GraphQuery) string {
3434

3535
var b strings.Builder
3636
x.Check2(b.WriteString("query {\n"))
37-
writeQuery(&b, query, " ", true)
37+
writeQuery(&b, query, " ")
3838
x.Check2(b.WriteString("}"))
3939

4040
return b.String()
4141
}
4242

43-
func writeQuery(b *strings.Builder, query *gql.GraphQuery, prefix string, root bool) {
43+
func writeQuery(b *strings.Builder, query *gql.GraphQuery, prefix string) {
4444
if query.Var != "" || query.Alias != "" || query.Attr != "" {
4545
x.Check2(b.WriteString(prefix))
4646
}
@@ -63,12 +63,16 @@ func writeQuery(b *strings.Builder, query *gql.GraphQuery, prefix string, root b
6363
x.Check2(b.WriteRune(')'))
6464
}
6565

66-
if !root && hasOrderOrPage(query) {
66+
if query.Func == nil && hasOrderOrPage(query) {
6767
x.Check2(b.WriteString(" ("))
6868
writeOrderAndPage(b, query, false)
6969
x.Check2(b.WriteRune(')'))
7070
}
7171

72+
if query.Cascade {
73+
x.Check2(b.WriteString(" @cascade"))
74+
}
75+
7276
switch {
7377
case len(query.Children) > 0:
7478
prefixAdd := ""
@@ -77,7 +81,7 @@ func writeQuery(b *strings.Builder, query *gql.GraphQuery, prefix string, root b
7781
prefixAdd = " "
7882
}
7983
for _, c := range query.Children {
80-
writeQuery(b, c, prefix+prefixAdd, false)
84+
writeQuery(b, c, prefix+prefixAdd)
8185
}
8286
if query.Attr != "" {
8387
x.Check2(b.WriteString(prefix))

0 commit comments

Comments
 (0)