upd(system):
- update apis for login and logout, - make OIDC routes and setup optional, - refactor entrypoint to open up http testing
This commit is contained in:
+24
-31
@@ -13,11 +13,33 @@
|
||||
"path": "/check",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"name": "login",
|
||||
"method": "POST",
|
||||
"title": "Login user",
|
||||
"path": "/login",
|
||||
"parameters": {
|
||||
"post": [
|
||||
{
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"title": "Username"
|
||||
},
|
||||
{
|
||||
"name": "password",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"title": "Password"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "logout",
|
||||
"method": "DELETE",
|
||||
"method": "GET",
|
||||
"title": "Delete JWT token (Sign Out)",
|
||||
"path": "/check",
|
||||
"path": "/logout",
|
||||
"parameters": {}
|
||||
}
|
||||
]
|
||||
@@ -381,35 +403,6 @@
|
||||
}
|
||||
],
|
||||
"apis": [
|
||||
{
|
||||
"name": "login",
|
||||
"method": "POST",
|
||||
"title": "Login user",
|
||||
"path": "/login",
|
||||
"parameters": {
|
||||
"post": [
|
||||
{
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"title": "Username"
|
||||
},
|
||||
{
|
||||
"name": "password",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"title": "Password"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "logout",
|
||||
"method": "GET",
|
||||
"title": "Delete JWT token (Sign Out)",
|
||||
"path": "/logout",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"name": "list",
|
||||
"method": "GET",
|
||||
|
||||
@@ -15,11 +15,33 @@
|
||||
"Path": "/check",
|
||||
"Parameters": {}
|
||||
},
|
||||
{
|
||||
"Name": "login",
|
||||
"Method": "POST",
|
||||
"Title": "Login user",
|
||||
"Path": "/login",
|
||||
"Parameters": {
|
||||
"post": [
|
||||
{
|
||||
"name": "username",
|
||||
"required": true,
|
||||
"title": "Username",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "password",
|
||||
"required": true,
|
||||
"title": "Password",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"Name": "logout",
|
||||
"Method": "DELETE",
|
||||
"Method": "GET",
|
||||
"Title": "Delete JWT token (Sign Out)",
|
||||
"Path": "/check",
|
||||
"Path": "/logout",
|
||||
"Parameters": {}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -17,35 +17,6 @@
|
||||
],
|
||||
"Path": "/users",
|
||||
"APIs": [
|
||||
{
|
||||
"Name": "login",
|
||||
"Method": "POST",
|
||||
"Title": "Login user",
|
||||
"Path": "/login",
|
||||
"Parameters": {
|
||||
"post": [
|
||||
{
|
||||
"name": "username",
|
||||
"required": true,
|
||||
"title": "Username",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "password",
|
||||
"required": true,
|
||||
"title": "Password",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"Name": "logout",
|
||||
"Method": "GET",
|
||||
"Title": "Delete JWT token (Sign Out)",
|
||||
"Path": "/logout",
|
||||
"Parameters": {}
|
||||
},
|
||||
{
|
||||
"Name": "list",
|
||||
"Method": "GET",
|
||||
|
||||
+16
-29
@@ -13,13 +13,28 @@
|
||||
| Parameter | Type | Method | Description | Default | Required? |
|
||||
| --------- | ---- | ------ | ----------- | ------- | --------- |
|
||||
|
||||
## Login user
|
||||
|
||||
#### Method
|
||||
|
||||
| URI | Protocol | Method | Authentication |
|
||||
| --- | -------- | ------ | -------------- |
|
||||
| `/auth/login` | HTTP/S | POST | |
|
||||
|
||||
#### Request parameters
|
||||
|
||||
| Parameter | Type | Method | Description | Default | Required? |
|
||||
| --------- | ---- | ------ | ----------- | ------- | --------- |
|
||||
| username | string | POST | Username | N/A | YES |
|
||||
| password | string | POST | Password | N/A | YES |
|
||||
|
||||
## Delete JWT token (Sign Out)
|
||||
|
||||
#### Method
|
||||
|
||||
| URI | Protocol | Method | Authentication |
|
||||
| --- | -------- | ------ | -------------- |
|
||||
| `/auth/check` | HTTP/S | DELETE | |
|
||||
| `/auth/logout` | HTTP/S | GET | |
|
||||
|
||||
#### Request parameters
|
||||
|
||||
@@ -277,34 +292,6 @@ An organisation may have many teams. Teams may have many channels available. Acc
|
||||
|
||||
# Users
|
||||
|
||||
## Login user
|
||||
|
||||
#### Method
|
||||
|
||||
| URI | Protocol | Method | Authentication |
|
||||
| --- | -------- | ------ | -------------- |
|
||||
| `/users/login` | HTTP/S | POST | Client ID, Session ID |
|
||||
|
||||
#### Request parameters
|
||||
|
||||
| Parameter | Type | Method | Description | Default | Required? |
|
||||
| --------- | ---- | ------ | ----------- | ------- | --------- |
|
||||
| username | string | POST | Username | N/A | YES |
|
||||
| password | string | POST | Password | N/A | YES |
|
||||
|
||||
## Delete JWT token (Sign Out)
|
||||
|
||||
#### Method
|
||||
|
||||
| URI | Protocol | Method | Authentication |
|
||||
| --- | -------- | ------ | -------------- |
|
||||
| `/users/logout` | HTTP/S | GET | Client ID, Session ID |
|
||||
|
||||
#### Request parameters
|
||||
|
||||
| Parameter | Type | Method | Description | Default | Required? |
|
||||
| --------- | ---- | ------ | ----------- | ------- | --------- |
|
||||
|
||||
## Search users (Directory)
|
||||
|
||||
#### Method
|
||||
|
||||
+36
-5
@@ -19,7 +19,7 @@ var _ = errors.Wrap
|
||||
|
||||
type (
|
||||
Auth struct {
|
||||
jwt jwtEncodeCookieSetter
|
||||
jwt auth.TokenEncoder
|
||||
}
|
||||
|
||||
checkResponse struct {
|
||||
@@ -36,6 +36,10 @@ func (ctrl *Auth) Check(ctx context.Context, r *request.AuthCheck) (interface{},
|
||||
return nil, errors.New("Not implemented: Auth.check")
|
||||
}
|
||||
|
||||
func (ctrl *Auth) Login(ctx context.Context, r *request.AuthLogin) (interface{}, error) {
|
||||
return nil, errors.New("Not implemented: Auth.login")
|
||||
}
|
||||
|
||||
func (ctrl *Auth) Logout(ctx context.Context, r *request.AuthLogout) (interface{}, error) {
|
||||
return nil, errors.New("Not implemented: Auth.logout")
|
||||
}
|
||||
@@ -43,7 +47,7 @@ func (ctrl *Auth) Logout(ctx context.Context, r *request.AuthLogout) (interface{
|
||||
// Handlers() func ignores "std" crust controllers
|
||||
//
|
||||
// Crush handlers are too abstract for our auth needs so we need (direct access to htt.ResponseWriter)
|
||||
func (ctrl *Auth) Handlers(jwtAuth jwtEncodeCookieSetter) *handlers.Auth {
|
||||
func (ctrl *Auth) Handlers(jwtEncoder auth.TokenEncoder) *handlers.Auth {
|
||||
h := handlers.NewAuth(ctrl)
|
||||
// Check JWT if valid
|
||||
h.Check = func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -52,7 +56,7 @@ func (ctrl *Auth) Handlers(jwtAuth jwtEncodeCookieSetter) *handlers.Auth {
|
||||
|
||||
if identity := auth.GetIdentityFromContext(ctx); identity != nil && identity.Valid() {
|
||||
if user, err := service.DefaultUser.With(ctx).FindByID(identity.Identity()); err == nil {
|
||||
jwtAuth.SetCookie(w, r, user)
|
||||
jwtEncoder.SetCookie(w, r, user)
|
||||
|
||||
resputil.JSON(w, checkResponse{
|
||||
JWT: c.Value,
|
||||
@@ -64,14 +68,41 @@ func (ctrl *Auth) Handlers(jwtAuth jwtEncodeCookieSetter) *handlers.Auth {
|
||||
}
|
||||
|
||||
// Did not send response, assuming invalid cookie
|
||||
jwtAuth.SetCookie(w, r, nil)
|
||||
jwtEncoder.SetCookie(w, r, nil)
|
||||
} else {
|
||||
resputil.JSON(w, err)
|
||||
}
|
||||
}
|
||||
|
||||
h.Logout = func(w http.ResponseWriter, r *http.Request) {
|
||||
jwtAuth.SetCookie(w, r, nil)
|
||||
jwtEncoder.SetCookie(w, r, nil)
|
||||
}
|
||||
h.Login = func(w http.ResponseWriter, r *http.Request) {
|
||||
params := request.NewAuthLogin()
|
||||
ctx := r.Context()
|
||||
|
||||
userSvc := service.User().With(ctx)
|
||||
|
||||
// check email and username for login
|
||||
user, err := userSvc.FindByEmail(params.Username)
|
||||
if err != nil {
|
||||
user, err = userSvc.FindByUsername(params.Username)
|
||||
}
|
||||
|
||||
// can't find user
|
||||
if err != nil {
|
||||
resputil.JSON(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// validate password
|
||||
if user.ValidatePassword(params.Password) {
|
||||
jwtEncoder.SetCookie(w, r, user)
|
||||
resputil.JSON(w, user, err)
|
||||
return
|
||||
}
|
||||
|
||||
resputil.JSON(w, errors.New("Password doesn't match"))
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
@@ -28,12 +28,14 @@ import (
|
||||
// Internal API interface
|
||||
type AuthAPI interface {
|
||||
Check(context.Context, *request.AuthCheck) (interface{}, error)
|
||||
Login(context.Context, *request.AuthLogin) (interface{}, error)
|
||||
Logout(context.Context, *request.AuthLogout) (interface{}, error)
|
||||
}
|
||||
|
||||
// HTTP API interface
|
||||
type Auth struct {
|
||||
Check func(http.ResponseWriter, *http.Request)
|
||||
Login func(http.ResponseWriter, *http.Request)
|
||||
Logout func(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
@@ -46,6 +48,13 @@ func NewAuth(ah AuthAPI) *Auth {
|
||||
return ah.Check(r.Context(), params)
|
||||
})
|
||||
},
|
||||
Login: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewAuthLogin()
|
||||
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
|
||||
return ah.Login(r.Context(), params)
|
||||
})
|
||||
},
|
||||
Logout: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewAuthLogout()
|
||||
@@ -61,7 +70,8 @@ func (ah *Auth) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http
|
||||
r.Use(middlewares...)
|
||||
r.Route("/auth", func(r chi.Router) {
|
||||
r.Get("/check", ah.Check)
|
||||
r.Delete("/check", ah.Logout)
|
||||
r.Post("/login", ah.Login)
|
||||
r.Get("/logout", ah.Logout)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -27,8 +27,6 @@ import (
|
||||
|
||||
// Internal API interface
|
||||
type UserAPI interface {
|
||||
Login(context.Context, *request.UserLogin) (interface{}, error)
|
||||
Logout(context.Context, *request.UserLogout) (interface{}, error)
|
||||
List(context.Context, *request.UserList) (interface{}, error)
|
||||
Create(context.Context, *request.UserCreate) (interface{}, error)
|
||||
Edit(context.Context, *request.UserEdit) (interface{}, error)
|
||||
@@ -40,8 +38,6 @@ type UserAPI interface {
|
||||
|
||||
// HTTP API interface
|
||||
type User struct {
|
||||
Login func(http.ResponseWriter, *http.Request)
|
||||
Logout func(http.ResponseWriter, *http.Request)
|
||||
List func(http.ResponseWriter, *http.Request)
|
||||
Create func(http.ResponseWriter, *http.Request)
|
||||
Edit func(http.ResponseWriter, *http.Request)
|
||||
@@ -53,20 +49,6 @@ type User struct {
|
||||
|
||||
func NewUser(uh UserAPI) *User {
|
||||
return &User{
|
||||
Login: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewUserLogin()
|
||||
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
|
||||
return uh.Login(r.Context(), params)
|
||||
})
|
||||
},
|
||||
Logout: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewUserLogout()
|
||||
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
|
||||
return uh.Logout(r.Context(), params)
|
||||
})
|
||||
},
|
||||
List: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewUserList()
|
||||
@@ -123,8 +105,6 @@ func (uh *User) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(middlewares...)
|
||||
r.Route("/users", func(r chi.Router) {
|
||||
r.Post("/login", uh.Login)
|
||||
r.Get("/logout", uh.Logout)
|
||||
r.Get("/", uh.List)
|
||||
r.Post("/", uh.Create)
|
||||
r.Put("/{userID}", uh.Edit)
|
||||
|
||||
+2
-7
@@ -32,7 +32,7 @@ type (
|
||||
|
||||
userService service.UserService
|
||||
|
||||
jwt jwtEncodeCookieSetter
|
||||
jwt auth.TokenEncoder
|
||||
}
|
||||
|
||||
oidcProfile struct {
|
||||
@@ -40,11 +40,6 @@ type (
|
||||
Name string `json:"name"`
|
||||
Sub string `json:"sub"`
|
||||
}
|
||||
|
||||
jwtEncodeCookieSetter interface {
|
||||
auth.TokenEncoder
|
||||
SetCookie(w http.ResponseWriter, r *http.Request, identity auth.Identifiable)
|
||||
}
|
||||
)
|
||||
|
||||
const openIdConnectStateCookie = "oidc-state"
|
||||
@@ -52,7 +47,7 @@ const openIdConnectStateCookie = "oidc-state"
|
||||
// Sets-up OIDC connection (issuer discovery, client registration)
|
||||
//
|
||||
// Client registration is done when no cfg.ClientID is provided.
|
||||
func OpenIdConnect(ctx context.Context, cfg *config.OIDC, usvc service.UserService, jwt jwtEncodeCookieSetter, settings repository.Settings) (c *openIdConnect, err error) {
|
||||
func OpenIdConnect(ctx context.Context, cfg *config.OIDC, usvc service.UserService, jwt auth.TokenEncoder, settings repository.Settings) (c *openIdConnect, err error) {
|
||||
c = &openIdConnect{
|
||||
appURL: cfg.AppURL,
|
||||
stateCookieExpiry: cfg.StateCookieExpiry,
|
||||
|
||||
@@ -70,6 +70,57 @@ func (a *AuthCheck) Fill(r *http.Request) (err error) {
|
||||
|
||||
var _ RequestFiller = NewAuthCheck()
|
||||
|
||||
// Auth login request parameters
|
||||
type AuthLogin struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
func NewAuthLogin() *AuthLogin {
|
||||
return &AuthLogin{}
|
||||
}
|
||||
|
||||
func (a *AuthLogin) Fill(r *http.Request) (err error) {
|
||||
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
|
||||
err = json.NewDecoder(r.Body).Decode(a)
|
||||
|
||||
switch {
|
||||
case err == io.EOF:
|
||||
err = nil
|
||||
case err != nil:
|
||||
return errors.Wrap(err, "error parsing http request body")
|
||||
}
|
||||
}
|
||||
|
||||
if err = r.ParseForm(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
get := map[string]string{}
|
||||
post := map[string]string{}
|
||||
urlQuery := r.URL.Query()
|
||||
for name, param := range urlQuery {
|
||||
get[name] = string(param[0])
|
||||
}
|
||||
postVars := r.Form
|
||||
for name, param := range postVars {
|
||||
post[name] = string(param[0])
|
||||
}
|
||||
|
||||
if val, ok := post["username"]; ok {
|
||||
|
||||
a.Username = val
|
||||
}
|
||||
if val, ok := post["password"]; ok {
|
||||
|
||||
a.Password = val
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
var _ RequestFiller = NewAuthLogin()
|
||||
|
||||
// Auth logout request parameters
|
||||
type AuthLogout struct {
|
||||
}
|
||||
|
||||
@@ -30,97 +30,6 @@ var _ = chi.URLParam
|
||||
var _ = types.JSONText{}
|
||||
var _ = multipart.FileHeader{}
|
||||
|
||||
// User login request parameters
|
||||
type UserLogin struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
func NewUserLogin() *UserLogin {
|
||||
return &UserLogin{}
|
||||
}
|
||||
|
||||
func (u *UserLogin) Fill(r *http.Request) (err error) {
|
||||
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
|
||||
err = json.NewDecoder(r.Body).Decode(u)
|
||||
|
||||
switch {
|
||||
case err == io.EOF:
|
||||
err = nil
|
||||
case err != nil:
|
||||
return errors.Wrap(err, "error parsing http request body")
|
||||
}
|
||||
}
|
||||
|
||||
if err = r.ParseForm(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
get := map[string]string{}
|
||||
post := map[string]string{}
|
||||
urlQuery := r.URL.Query()
|
||||
for name, param := range urlQuery {
|
||||
get[name] = string(param[0])
|
||||
}
|
||||
postVars := r.Form
|
||||
for name, param := range postVars {
|
||||
post[name] = string(param[0])
|
||||
}
|
||||
|
||||
if val, ok := post["username"]; ok {
|
||||
|
||||
u.Username = val
|
||||
}
|
||||
if val, ok := post["password"]; ok {
|
||||
|
||||
u.Password = val
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
var _ RequestFiller = NewUserLogin()
|
||||
|
||||
// User logout request parameters
|
||||
type UserLogout struct {
|
||||
}
|
||||
|
||||
func NewUserLogout() *UserLogout {
|
||||
return &UserLogout{}
|
||||
}
|
||||
|
||||
func (u *UserLogout) Fill(r *http.Request) (err error) {
|
||||
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
|
||||
err = json.NewDecoder(r.Body).Decode(u)
|
||||
|
||||
switch {
|
||||
case err == io.EOF:
|
||||
err = nil
|
||||
case err != nil:
|
||||
return errors.Wrap(err, "error parsing http request body")
|
||||
}
|
||||
}
|
||||
|
||||
if err = r.ParseForm(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
get := map[string]string{}
|
||||
post := map[string]string{}
|
||||
urlQuery := r.URL.Query()
|
||||
for name, param := range urlQuery {
|
||||
get[name] = string(param[0])
|
||||
}
|
||||
postVars := r.Form
|
||||
for name, param := range postVars {
|
||||
post[name] = string(param[0])
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
var _ RequestFiller = NewUserLogout()
|
||||
|
||||
// User list request parameters
|
||||
type UserList struct {
|
||||
Query string
|
||||
|
||||
+12
-6
@@ -13,18 +13,24 @@ import (
|
||||
"github.com/crusttech/crust/system/service"
|
||||
)
|
||||
|
||||
func MountRoutes(oidcConfig *config.OIDC, jwtAuth jwtEncodeCookieSetter) func(chi.Router) {
|
||||
func MountRoutes(oidcConfig *config.OIDC, jwtEncoder auth.TokenEncoder) func(chi.Router) {
|
||||
var err error
|
||||
var userSvc = service.User()
|
||||
var ctx = context.Background()
|
||||
var oidc *openIdConnect
|
||||
|
||||
oidc, err := OpenIdConnect(ctx, oidcConfig, userSvc, jwtAuth, repository.NewSettings(ctx, repository.DB(ctx)))
|
||||
if err != nil {
|
||||
log.Print("Could not initialize OIDC:", err.Error())
|
||||
if (oidcConfig.Enabled) {
|
||||
oidc, err = OpenIdConnect(ctx, oidcConfig, userSvc, jwtEncoder, repository.NewSettings(ctx, repository.DB(ctx)))
|
||||
if err != nil {
|
||||
log.Println("Could not initialize OIDC:", err.Error())
|
||||
}
|
||||
} else {
|
||||
log.Println("OIDC is disabled")
|
||||
}
|
||||
|
||||
// Initialize handers & controllers.
|
||||
return func(r chi.Router) {
|
||||
if oidc != nil {
|
||||
if oidcConfig.Enabled && oidc != nil {
|
||||
r.Route("/oidc", func(r chi.Router) {
|
||||
r.Get("/", oidc.HandleRedirect)
|
||||
r.Get("/callback", oidc.HandleOAuth2Callback)
|
||||
@@ -32,7 +38,7 @@ func MountRoutes(oidcConfig *config.OIDC, jwtAuth jwtEncodeCookieSetter) func(ch
|
||||
}
|
||||
|
||||
// Provide raw `/auth` handlers
|
||||
Auth{}.New().Handlers(jwtAuth).MountRoutes(r)
|
||||
Auth{}.New().Handlers(jwtEncoder).MountRoutes(r)
|
||||
|
||||
// Protect all _private_ routes
|
||||
r.Group(func(r chi.Router) {
|
||||
|
||||
@@ -29,14 +29,6 @@ func (ctrl *User) List(ctx context.Context, r *request.UserList) (interface{}, e
|
||||
return ctrl.user.With(ctx).Find(&types.UserFilter{Query: r.Query})
|
||||
}
|
||||
|
||||
func (ctrl *User) Login(ctx context.Context, r *request.UserLogin) (interface{}, error) {
|
||||
return nil, errors.New("Not implemented: User.Login")
|
||||
}
|
||||
|
||||
func (ctrl *User) Logout(ctx context.Context, r *request.UserLogout) (interface{}, error) {
|
||||
return nil, errors.New("Not implemented: User.Logout")
|
||||
}
|
||||
|
||||
func (ctrl *User) Create(ctx context.Context, r *request.UserCreate) (interface{}, error) {
|
||||
user := &types.User{
|
||||
Email: r.Email,
|
||||
|
||||
@@ -5,15 +5,34 @@ import (
|
||||
"reflect"
|
||||
"runtime"
|
||||
|
||||
"net/http"
|
||||
|
||||
"github.com/99designs/basicauth-go"
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"github.com/go-chi/cors"
|
||||
|
||||
"github.com/crusttech/crust/internal/config"
|
||||
"github.com/crusttech/crust/internal/metrics"
|
||||
"github.com/crusttech/crust/internal/version"
|
||||
"github.com/crusttech/crust/system/rest"
|
||||
)
|
||||
|
||||
func Routes() *chi.Mux {
|
||||
r := chi.NewRouter()
|
||||
r.Use(handleCORS)
|
||||
|
||||
// Only protect application routes with JWT
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(jwtVerifier, jwtAuthenticator)
|
||||
mountRoutes(r, flags.http, rest.MountRoutes(flags.oidc, jwtEncoder))
|
||||
})
|
||||
|
||||
printRoutes(r, flags.http)
|
||||
mountSystemRoutes(r, flags.http)
|
||||
return r
|
||||
}
|
||||
|
||||
func mountRoutes(r chi.Router, opts *config.HTTP, mounts ...func(r chi.Router)) {
|
||||
if opts.Logging {
|
||||
r.Use(middleware.Logger)
|
||||
@@ -57,3 +76,14 @@ func printRoutes(r chi.Router, opts *config.HTTP) {
|
||||
}
|
||||
printRoutes(r, "", "")
|
||||
}
|
||||
|
||||
// Sets up default CORS rules to use as a middleware
|
||||
func handleCORS(next http.Handler) http.Handler {
|
||||
return cors.New(cors.Options{
|
||||
AllowedOrigins: []string{"*"},
|
||||
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"},
|
||||
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
|
||||
AllowCredentials: true,
|
||||
MaxAge: 300, // Maximum value not ignored by any of major browsers
|
||||
}).Handler(next)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/http/httputil"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/namsral/flag"
|
||||
|
||||
"github.com/crusttech/crust/internal/auth"
|
||||
"github.com/crusttech/crust/internal/rbac"
|
||||
)
|
||||
|
||||
func TestUsers(t *testing.T) {
|
||||
godotenv.Load("../.env")
|
||||
|
||||
mountFlags("system", Flags, auth.Flags, rbac.Flags)
|
||||
|
||||
// log to stdout not stderr
|
||||
log.SetOutput(os.Stdout)
|
||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||
|
||||
err := Init()
|
||||
assert(t, err == nil, "Error initializing: %+v", err)
|
||||
|
||||
req, err := http.NewRequest("GET", "http://localhost/auth/check", nil)
|
||||
assert(t, err == nil, "Error creating request: %+v", err)
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
Routes().ServeHTTP(recorder, req)
|
||||
|
||||
resp := recorder.Result()
|
||||
|
||||
fmt.Println(">>> (request)")
|
||||
fmt.Println(request(req))
|
||||
fmt.Println("----")
|
||||
fmt.Println("<<< (response)")
|
||||
fmt.Println(response(resp))
|
||||
}
|
||||
|
||||
func request(req *http.Request) string {
|
||||
b, err := httputil.DumpRequestOut(req, true)
|
||||
if err != nil {
|
||||
return ">>> Error: " + err.Error()
|
||||
}
|
||||
if b != nil {
|
||||
return strings.TrimSpace(string(b))
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func response(resp *http.Response) string {
|
||||
b, err := httputil.DumpResponse(resp, true)
|
||||
if err != nil {
|
||||
return "<<< Error: " + err.Error()
|
||||
}
|
||||
if b != nil {
|
||||
return strings.TrimSpace(string(b))
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func assert(t *testing.T, ok bool, format string, args ...interface{}) bool {
|
||||
if !ok {
|
||||
t.Fatalf(format, args...)
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
func mountFlags(prefix string, mountFlags ...func(...string)) {
|
||||
for _, mount := range mountFlags {
|
||||
mount(prefix)
|
||||
}
|
||||
flag.Parse()
|
||||
}
|
||||
+12
-1
@@ -28,9 +28,13 @@ type (
|
||||
UserService interface {
|
||||
With(ctx context.Context) UserService
|
||||
|
||||
FindByUsername(username string) (*types.User, error)
|
||||
FindByEmail(email string) (*types.User, error)
|
||||
FindByID(id uint64) (*types.User, error)
|
||||
Find(filter *types.UserFilter) (types.UserSet, error)
|
||||
|
||||
FindOrCreate(*types.User) (*types.User, error)
|
||||
|
||||
Create(input *types.User) (*types.User, error)
|
||||
Update(mod *types.User) (*types.User, error)
|
||||
|
||||
@@ -38,7 +42,6 @@ type (
|
||||
Suspend(id uint64) error
|
||||
Unsuspend(id uint64) error
|
||||
|
||||
FindOrCreate(*types.User) (*types.User, error)
|
||||
ValidateCredentials(username, password string) (*types.User, error)
|
||||
}
|
||||
)
|
||||
@@ -90,6 +93,14 @@ func (svc *user) FindByID(id uint64) (*types.User, error) {
|
||||
return svc.user.FindByID(id)
|
||||
}
|
||||
|
||||
func (svc *user) FindByEmail(email string) (*types.User, error) {
|
||||
return svc.user.FindByEmail(email)
|
||||
}
|
||||
|
||||
func (svc *user) FindByUsername(username string) (*types.User, error) {
|
||||
return svc.user.FindByUsername(username)
|
||||
}
|
||||
|
||||
func (svc *user) Find(filter *types.UserFilter) (types.UserSet, error) {
|
||||
return svc.user.Find(filter)
|
||||
}
|
||||
|
||||
+18
-40
@@ -7,14 +7,11 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/SentimensRG/ctx/sigctx"
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/cors"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/titpetric/factory"
|
||||
"github.com/titpetric/factory/resputil"
|
||||
|
||||
migrate "github.com/crusttech/crust/system/db"
|
||||
"github.com/crusttech/crust/system/rest"
|
||||
"github.com/crusttech/crust/system/service"
|
||||
|
||||
"github.com/crusttech/crust/internal/auth"
|
||||
@@ -22,25 +19,36 @@ import (
|
||||
"github.com/crusttech/crust/internal/version"
|
||||
)
|
||||
|
||||
var (
|
||||
jwtVerifier (func(http.Handler) http.Handler)
|
||||
jwtAuthenticator (func(http.Handler) http.Handler)
|
||||
jwtEncoder auth.TokenEncoder
|
||||
)
|
||||
|
||||
func Init() error {
|
||||
// validate configuration
|
||||
if err := flags.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// JWT Auth
|
||||
jwtEncoder, err := auth.JWT()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Error creating JWT Auth object")
|
||||
}
|
||||
jwtVerifier = jwtEncoder.Verifier()
|
||||
jwtAuthenticator = jwtEncoder.Authenticator()
|
||||
|
||||
// start/configure database connection
|
||||
factory.Database.Add("default", flags.db.DSN)
|
||||
db, err := factory.Database.Get()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
db := factory.Database.MustGet()
|
||||
|
||||
// @todo: profiling as an external service?
|
||||
switch flags.db.Profiler {
|
||||
case "stdout":
|
||||
db.Profiler = &factory.Database.ProfilerStdout
|
||||
default:
|
||||
fmt.Println("No database query profiler selected")
|
||||
log.Println("No database query profiler selected")
|
||||
}
|
||||
|
||||
// migrate database schema
|
||||
@@ -63,8 +71,6 @@ func Init() error {
|
||||
}
|
||||
|
||||
func Start() error {
|
||||
var deadline = sigctx.New()
|
||||
|
||||
log.Printf("Starting auth, version: %v, built on: %v", version.Version, version.BuildTime)
|
||||
log.Println("Starting http server on address " + flags.http.Addr)
|
||||
listener, err := net.Listen("tcp", flags.http.Addr)
|
||||
@@ -72,41 +78,13 @@ func Start() error {
|
||||
return errors.Wrap(err, fmt.Sprintf("Can't listen on addr %s", flags.http.Addr))
|
||||
}
|
||||
|
||||
// JWT Auth
|
||||
jwtAuth, err := auth.JWT()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Error creating JWT Auth object")
|
||||
}
|
||||
|
||||
r := chi.NewRouter()
|
||||
r.Use(handleCORS)
|
||||
|
||||
// Only protect application routes with JWT
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(jwtAuth.Verifier(), jwtAuth.Authenticator())
|
||||
mountRoutes(r, flags.http, rest.MountRoutes(flags.oidc, jwtAuth))
|
||||
})
|
||||
|
||||
printRoutes(r, flags.http)
|
||||
mountSystemRoutes(r, flags.http)
|
||||
|
||||
if flags.monitor.Interval > 0 {
|
||||
go metrics.NewMonitor(flags.monitor.Interval)
|
||||
}
|
||||
go http.Serve(listener, Routes())
|
||||
|
||||
go http.Serve(listener, r)
|
||||
var deadline = sigctx.New()
|
||||
<-deadline.Done()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sets up default CORS rules to use as a middleware
|
||||
func handleCORS(next http.Handler) http.Handler {
|
||||
return cors.New(cors.Options{
|
||||
AllowedOrigins: []string{"*"},
|
||||
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"},
|
||||
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
|
||||
AllowCredentials: true,
|
||||
MaxAge: 300, // Maximum value not ignored by any of major browsers
|
||||
}).Handler(next)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user