mirror of https://git.sr.ht/~sircmpwn/gql.sr.ht
Implement OAuth 2.0 bearer token w/scopes
This commit is contained in:
parent
0395c9720d
commit
8eb53f35aa
|
@ -50,7 +50,7 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
AUTH_OAUTH = iota
|
AUTH_OAUTH_LEGACY = iota
|
||||||
AUTH_OAUTH2 = iota
|
AUTH_OAUTH2 = iota
|
||||||
AUTH_COOKIE = iota
|
AUTH_COOKIE = iota
|
||||||
AUTH_INTERNAL = iota
|
AUTH_INTERNAL = iota
|
||||||
|
@ -75,6 +75,7 @@ type User struct {
|
||||||
|
|
||||||
// Only filled out if AuthMethod == AUTH_OAUTH2
|
// Only filled out if AuthMethod == AUTH_OAUTH2
|
||||||
OAuth2Token *OAuth2Token
|
OAuth2Token *OAuth2Token
|
||||||
|
Access map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
func authError(w http.ResponseWriter, reason string, code int) {
|
func authError(w http.ResponseWriter, reason string, code int) {
|
||||||
|
@ -404,6 +405,32 @@ func OAuth2(db *sql.DB, token string, hash [64]byte,
|
||||||
user.AuthMethod = AUTH_OAUTH2
|
user.AuthMethod = AUTH_OAUTH2
|
||||||
user.OAuth2Token = ot
|
user.OAuth2Token = ot
|
||||||
|
|
||||||
|
if ot.Scopes != "" {
|
||||||
|
user.Access = make(map[string]string)
|
||||||
|
for _, grant := range strings.Split(ot.Scopes, ",") {
|
||||||
|
var (
|
||||||
|
service string
|
||||||
|
scope string
|
||||||
|
access string
|
||||||
|
)
|
||||||
|
parts := strings.Split(grant, "/")
|
||||||
|
if len(parts) != 2 {
|
||||||
|
panic(errors.New("OAuth grant without service/scope format"))
|
||||||
|
}
|
||||||
|
service = parts[0]
|
||||||
|
parts = strings.Split(parts[1], ":")
|
||||||
|
scope = parts[0]
|
||||||
|
if len(parts) == 1 {
|
||||||
|
access = "RO"
|
||||||
|
} else {
|
||||||
|
access = parts[1]
|
||||||
|
}
|
||||||
|
if service == config.ServiceName(r.Context()) {
|
||||||
|
user.Access[scope] = access
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ctx := context.WithValue(r.Context(), userCtxKey, &user)
|
ctx := context.WithValue(r.Context(), userCtxKey, &user)
|
||||||
|
|
||||||
r = r.WithContext(ctx)
|
r = r.WithContext(ctx)
|
||||||
|
@ -481,7 +508,7 @@ func LegacyOAuth(db *sql.DB, bearer string, hash [64]byte,
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user.AuthMethod = AUTH_OAUTH
|
user.AuthMethod = AUTH_OAUTH_LEGACY
|
||||||
|
|
||||||
ctx := context.WithValue(r.Context(), userCtxKey, &user)
|
ctx := context.WithValue(r.Context(), userCtxKey, &user)
|
||||||
|
|
||||||
|
|
|
@ -9,15 +9,18 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var configCtxKey = &contextKey{"config"}
|
var configCtxKey = &contextKey{"config"}
|
||||||
|
var serviceCtxKey = &contextKey{"name"}
|
||||||
|
|
||||||
type contextKey struct {
|
type contextKey struct {
|
||||||
name string
|
name string
|
||||||
}
|
}
|
||||||
|
|
||||||
func Middleware(conf ini.File) func (next http.Handler) http.Handler {
|
func Middleware(conf ini.File, service string) func (next http.Handler) http.Handler {
|
||||||
|
svc := service
|
||||||
return func (next http.Handler) http.Handler {
|
return func (next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := context.WithValue(r.Context(), configCtxKey, conf)
|
ctx := context.WithValue(r.Context(), configCtxKey, conf)
|
||||||
|
ctx = context.WithValue(ctx, serviceCtxKey, &svc)
|
||||||
r = r.WithContext(ctx)
|
r = r.WithContext(ctx)
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
})
|
})
|
||||||
|
@ -31,3 +34,11 @@ func ForContext(ctx context.Context) ini.File {
|
||||||
}
|
}
|
||||||
return raw
|
return raw
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ServiceName(ctx context.Context) string {
|
||||||
|
raw, ok := ctx.Value(serviceCtxKey).(*string)
|
||||||
|
if !ok {
|
||||||
|
panic(errors.New("Invalid config context"))
|
||||||
|
}
|
||||||
|
return *raw
|
||||||
|
}
|
||||||
|
|
|
@ -22,10 +22,30 @@ func Internal(ctx context.Context, obj interface{},
|
||||||
func Access(ctx context.Context, obj interface{}, next graphql.Resolver,
|
func Access(ctx context.Context, obj interface{}, next graphql.Resolver,
|
||||||
scope string, kind string) (interface{}, error) {
|
scope string, kind string) (interface{}, error) {
|
||||||
|
|
||||||
if auth.ForContext(ctx).AuthMethod == auth.AUTH_INTERNAL ||
|
authctx := auth.ForContext(ctx)
|
||||||
auth.ForContext(ctx).AuthMethod == auth.AUTH_COOKIE {
|
|
||||||
|
switch authctx.AuthMethod {
|
||||||
|
case auth.AUTH_INTERNAL:
|
||||||
|
case auth.AUTH_COOKIE:
|
||||||
return next(ctx)
|
return next(ctx)
|
||||||
|
case auth.AUTH_OAUTH_LEGACY:
|
||||||
|
if kind == "RO" {
|
||||||
|
// Only legacy tokens with "*" scopes ever get this far
|
||||||
|
return next(ctx)
|
||||||
|
}
|
||||||
|
case auth.AUTH_OAUTH2:
|
||||||
|
if authctx.Access == nil {
|
||||||
|
return next(ctx)
|
||||||
|
}
|
||||||
|
if access, ok := authctx.Access[scope]; !ok {
|
||||||
|
break
|
||||||
|
} else if access == "RO" && kind == "RW" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return next(ctx)
|
||||||
|
default:
|
||||||
|
panic(fmt.Errorf("Unknown auth method for access check"))
|
||||||
}
|
}
|
||||||
|
|
||||||
panic(fmt.Errorf("TODO"))
|
return nil, fmt.Errorf("Access denied")
|
||||||
}
|
}
|
||||||
|
|
|
@ -130,7 +130,7 @@ func MakeRouter(service string, conf ini.File, schema graphql.ExecutableSchema,
|
||||||
requestsProcessed.Inc()
|
requestsProcessed.Inc()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
router.Use(config.Middleware(conf))
|
router.Use(config.Middleware(conf, service))
|
||||||
router.Use(database.Middleware(db))
|
router.Use(database.Middleware(db))
|
||||||
router.Use(redis.Middleware(rc))
|
router.Use(redis.Middleware(rc))
|
||||||
router.Use(middleware.RealIP)
|
router.Use(middleware.RealIP)
|
||||||
|
|
Loading…
Reference in New Issue