gateway: add user registration

Adds two handler:

* Authorize handler used to ask remote source authorization
* Register handler used to do user and related linked account creation
This commit is contained in:
Simone Gotti 2019-03-29 17:53:15 +01:00
parent 704da47afc
commit eb8cd9cc52
5 changed files with 329 additions and 20 deletions

View File

@ -273,6 +273,17 @@ func (c *Client) DeleteUserLA(ctx context.Context, userName, laID string) (*http
return c.getResponse(ctx, "DELETE", fmt.Sprintf("/users/%s/linkedaccounts/%s", userName, laID), nil, jsonContent, nil)
}
func (c *Client) RegisterUser(ctx context.Context, req *RegisterUserRequest) (*RegisterUserResponse, *http.Response, error) {
reqj, err := json.Marshal(req)
if err != nil {
return nil, nil, err
}
res := new(RegisterUserResponse)
resp, err := c.getParsedResponse(ctx, "PUT", "/register", nil, jsonContent, bytes.NewReader(reqj), res)
return res, resp, err
}
func (c *Client) CreateUserToken(ctx context.Context, userName string, req *CreateUserTokenRequest) (*CreateUserTokenResponse, *http.Response, error) {
reqj, err := json.Marshal(req)
if err != nil {

View File

@ -54,22 +54,32 @@ func (h *OAuth2CallbackHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
var response interface{}
switch cresp.RequestType {
case "createuserla":
case command.RemoteSourceRequestTypeCreateUserLA:
authresp := cresp.Response.(*command.CreateUserLAResponse)
response = &CreateUserLAResponse{
LinkedAccount: authresp.LinkedAccount,
}
case "loginuser":
case command.RemoteSourceRequestTypeLoginUser:
authresp := cresp.Response.(*command.LoginUserResponse)
response = &LoginUserResponse{
Token: authresp.Token,
User: createUserResponse(authresp.User),
}
case command.RemoteSourceRequestTypeAuthorize:
authresp := cresp.Response.(*command.AuthorizeResponse)
response = &AuthorizeResponse{
RemoteUserInfo: authresp.RemoteUserInfo,
RemoteSourceName: authresp.RemoteSourceName,
}
case command.RemoteSourceRequestTypeRegisterUser:
response = &RegisterUserResponse{}
}
resp := RemoteSourceAuthResult{
RequestType: cresp.RequestType,
RequestType: string(cresp.RequestType),
Response: response,
}
if err := json.NewEncoder(w).Encode(resp); err != nil {

View File

@ -20,6 +20,7 @@ import (
"net/http"
"strconv"
gitsource "github.com/sorintlab/agola/internal/gitsources"
csapi "github.com/sorintlab/agola/internal/services/configstore/api"
"github.com/sorintlab/agola/internal/services/gateway/command"
"github.com/sorintlab/agola/internal/services/types"
@ -266,8 +267,8 @@ func (h *UsersHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
type CreateUserLARequest struct {
RemoteSourceName string `json:"remote_source_name"`
RemoteSourceLoginName string `json:"remote_login_name"`
RemoteSourceLoginPassword string `json:"remote_login_password"`
RemoteSourceLoginName string `json:"remote_source_login_name"`
RemoteSourceLoginPassword string `json:"remote_source_login_password"`
}
type CreateUserLAResponse struct {
@ -317,7 +318,7 @@ func (h *CreateUserLAHandler) createUserLA(ctx context.Context, userName string,
}
h.log.Infof("creating linked account")
cresp, err := h.ch.HandleRemoteSourceAuth(ctx, req.RemoteSourceName, req.RemoteSourceLoginName, req.RemoteSourceLoginPassword, "createuserla", creq)
cresp, err := h.ch.HandleRemoteSourceAuth(ctx, req.RemoteSourceName, req.RemoteSourceLoginName, req.RemoteSourceLoginPassword, command.RemoteSourceRequestTypeCreateUserLA, creq)
if err != nil {
return nil, err
}
@ -407,6 +408,131 @@ func (h *CreateUserTokenHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
}
}
type RegisterUserRequest struct {
CreateUserRequest
CreateUserLARequest
}
type RegisterUserHandler struct {
log *zap.SugaredLogger
ch *command.CommandHandler
}
type RegisterUserResponse struct {
Oauth2Redirect string `json:"oauth2_redirect"`
}
func NewRegisterUserHandler(logger *zap.Logger, ch *command.CommandHandler) *RegisterUserHandler {
return &RegisterUserHandler{log: logger.Sugar(), ch: ch}
}
func (h *RegisterUserHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var req *RegisterUserRequest
d := json.NewDecoder(r.Body)
if err := d.Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
resp, err := h.registerUser(ctx, req)
if err != nil {
h.log.Errorf("err: %+v", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func (h *RegisterUserHandler) registerUser(ctx context.Context, req *RegisterUserRequest) (*RegisterUserResponse, error) {
creq := &command.RegisterUserRequest{
UserName: req.CreateUserRequest.UserName,
RemoteSourceName: req.CreateUserLARequest.RemoteSourceName,
}
cresp, err := h.ch.HandleRemoteSourceAuth(ctx, req.CreateUserLARequest.RemoteSourceName, req.CreateUserLARequest.RemoteSourceLoginName, req.CreateUserLARequest.RemoteSourceLoginPassword, command.RemoteSourceRequestTypeRegisterUser, creq)
if err != nil {
return nil, err
}
if cresp.Oauth2Redirect != "" {
return &RegisterUserResponse{
Oauth2Redirect: cresp.Oauth2Redirect,
}, nil
}
//authresp := cresp.Response.(*command.RegisterUserResponse)
resp := &RegisterUserResponse{}
return resp, nil
}
type AuthorizeHandler struct {
log *zap.SugaredLogger
ch *command.CommandHandler
}
type AuthorizeResponse struct {
Oauth2Redirect string `json:"oauth2_redirect"`
RemoteUserInfo *gitsource.UserInfo `json:"remote_user_info"`
RemoteSourceName string `json:"remote_source_name"`
}
func NewAuthorizeHandler(logger *zap.Logger, ch *command.CommandHandler) *AuthorizeHandler {
return &AuthorizeHandler{log: logger.Sugar(), ch: ch}
}
func (h *AuthorizeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var req *LoginUserRequest
d := json.NewDecoder(r.Body)
if err := d.Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
resp, err := h.authorize(ctx, req)
if err != nil {
h.log.Errorf("err: %+v", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func (h *AuthorizeHandler) authorize(ctx context.Context, req *LoginUserRequest) (*AuthorizeResponse, error) {
creq := &command.LoginUserRequest{
RemoteSourceName: req.RemoteSourceName,
}
cresp, err := h.ch.HandleRemoteSourceAuth(ctx, req.RemoteSourceName, req.LoginName, req.LoginPassword, command.RemoteSourceRequestTypeAuthorize, creq)
if err != nil {
return nil, err
}
if cresp.Oauth2Redirect != "" {
return &AuthorizeResponse{
Oauth2Redirect: cresp.Oauth2Redirect,
}, nil
}
authresp := cresp.Response.(*command.AuthorizeResponse)
resp := &AuthorizeResponse{
RemoteUserInfo: authresp.RemoteUserInfo,
RemoteSourceName: authresp.RemoteSourceName,
}
return resp, nil
}
type LoginUserRequest struct {
RemoteSourceName string `json:"remote_source_name"`
LoginName string `json:"login_name"`
@ -459,7 +585,7 @@ func (h *LoginUserHandler) loginUser(ctx context.Context, req *LoginUserRequest)
}
h.log.Infof("logging in user")
cresp, err := h.ch.HandleRemoteSourceAuth(ctx, req.RemoteSourceName, req.LoginName, req.LoginPassword, "loginuser", creq)
cresp, err := h.ch.HandleRemoteSourceAuth(ctx, req.RemoteSourceName, req.LoginName, req.LoginPassword, command.RemoteSourceRequestTypeLoginUser, creq)
if err != nil {
return nil, err
}

View File

@ -18,6 +18,7 @@ import (
"context"
"encoding/json"
gitsource "github.com/sorintlab/agola/internal/gitsources"
csapi "github.com/sorintlab/agola/internal/services/configstore/api"
"github.com/sorintlab/agola/internal/services/gateway/common"
"github.com/sorintlab/agola/internal/services/types"
@ -120,12 +121,74 @@ func (c *CommandHandler) CreateUserLA(ctx context.Context, req *CreateUserLARequ
return la, nil
}
type RegisterUserRequest struct {
UserName string
RemoteSourceName string
RemoteSourceUserAccessToken string
RemoteSourceOauth2AccessToken string
RemoteSourceOauth2RefreshToken string
}
func (c *CommandHandler) RegisterUser(ctx context.Context, req *RegisterUserRequest) (*types.User, error) {
if req.UserName == "" {
return nil, util.NewErrBadRequest(errors.Errorf("user name required"))
}
if !util.ValidateName(req.UserName) {
return nil, errors.Errorf("invalid user name %q", req.UserName)
}
rs, _, err := c.configstoreClient.GetRemoteSourceByName(ctx, req.RemoteSourceName)
if err != nil {
return nil, errors.Wrapf(err, "failed to get remote source %q", req.RemoteSourceName)
}
c.log.Infof("rs: %s", util.Dump(rs))
accessToken, err := common.GetAccessToken(rs.AuthType, req.RemoteSourceUserAccessToken, req.RemoteSourceOauth2AccessToken)
if err != nil {
return nil, err
}
userSource, err := common.GetUserSource(rs, accessToken)
if err != nil {
return nil, err
}
remoteUserInfo, err := userSource.GetUserInfo()
if err != nil {
return nil, errors.Wrapf(err, "failed to retrieve remote user info for remote source %q", rs.ID)
}
if remoteUserInfo.ID == "" {
return nil, errors.Errorf("empty remote user id for remote source %q", rs.ID)
}
creq := &csapi.CreateUserRequest{
UserName: req.UserName,
CreateUserLARequest: &csapi.CreateUserLARequest{
RemoteSourceName: req.RemoteSourceName,
RemoteUserID: remoteUserInfo.ID,
RemoteUserName: remoteUserInfo.LoginName,
Oauth2AccessToken: req.RemoteSourceOauth2AccessToken,
Oauth2RefreshToken: req.RemoteSourceOauth2RefreshToken,
UserAccessToken: req.RemoteSourceUserAccessToken,
},
}
c.log.Infof("creating user account")
u, _, err := c.configstoreClient.CreateUser(ctx, creq)
if err != nil {
return nil, errors.Wrapf(err, "failed to create linked account")
}
c.log.Infof("user %q created", req.UserName)
return u, nil
}
type LoginUserRequest struct {
RemoteSourceName string
RemoteSourceUserAccessToken string
RemoteSourceOauth2AccessToken string
RemoteSourceOauth2RefreshToken string
}
type LoginUserResponse struct {
Token string
User *types.User
@ -208,12 +271,54 @@ func (c *CommandHandler) LoginUser(ctx context.Context, req *LoginUserRequest) (
}, nil
}
type AuthorizeRequest struct {
RemoteSourceName string
RemoteSourceUserAccessToken string
RemoteSourceOauth2AccessToken string
RemoteSourceOauth2RefreshToken string
}
type AuthorizeResponse struct {
RemoteUserInfo *gitsource.UserInfo
RemoteSourceName string
}
func (c *CommandHandler) Authorize(ctx context.Context, req *AuthorizeRequest) (*AuthorizeResponse, error) {
rs, _, err := c.configstoreClient.GetRemoteSourceByName(ctx, req.RemoteSourceName)
if err != nil {
return nil, errors.Wrapf(err, "failed to get remote source %q", req.RemoteSourceName)
}
c.log.Infof("rs: %s", util.Dump(rs))
accessToken, err := common.GetAccessToken(rs.AuthType, req.RemoteSourceUserAccessToken, req.RemoteSourceOauth2AccessToken)
if err != nil {
return nil, err
}
userSource, err := common.GetUserSource(rs, accessToken)
if err != nil {
return nil, err
}
remoteUserInfo, err := userSource.GetUserInfo()
if err != nil {
return nil, errors.Wrapf(err, "failed to retrieve remote user info for remote source %q", rs.ID)
}
if remoteUserInfo.ID == "" {
return nil, errors.Errorf("empty remote user id for remote source %q", rs.ID)
}
return &AuthorizeResponse{
RemoteUserInfo: remoteUserInfo,
RemoteSourceName: req.RemoteSourceName,
}, nil
}
type RemoteSourceAuthResponse struct {
Oauth2Redirect string
Response interface{}
}
func (c *CommandHandler) HandleRemoteSourceAuth(ctx context.Context, remoteSourceName, loginName, loginPassword, requestType string, req interface{}) (*RemoteSourceAuthResponse, error) {
func (c *CommandHandler) HandleRemoteSourceAuth(ctx context.Context, remoteSourceName, loginName, loginPassword string, requestType RemoteSourceRequestType, req interface{}) (*RemoteSourceAuthResponse, error) {
rs, _, err := c.configstoreClient.GetRemoteSourceByName(ctx, remoteSourceName)
if err != nil {
return nil, errors.Wrapf(err, "failed to get remote source %q", remoteSourceName)
@ -221,7 +326,7 @@ func (c *CommandHandler) HandleRemoteSourceAuth(ctx context.Context, remoteSourc
c.log.Infof("rs: %s", util.Dump(rs))
switch requestType {
case "createuserla":
case RemoteSourceRequestTypeCreateUserLA:
req := req.(*CreateUserLARequest)
user, _, err := c.configstoreClient.GetUserByName(ctx, req.UserName)
if err != nil {
@ -239,7 +344,11 @@ func (c *CommandHandler) HandleRemoteSourceAuth(ctx context.Context, remoteSourc
return nil, errors.Errorf("user %q already have a linked account for remote source %q", req.UserName, rs.Name)
}
case "loginuser":
case RemoteSourceRequestTypeLoginUser:
case RemoteSourceRequestTypeAuthorize:
case RemoteSourceRequestTypeRegisterUser:
default:
return nil, errors.Errorf("unknown request type: %q", requestType)
@ -251,7 +360,7 @@ func (c *CommandHandler) HandleRemoteSourceAuth(ctx context.Context, remoteSourc
if err != nil {
return nil, errors.Wrapf(err, "failed to create git source")
}
token, err := common.GenerateJWTToken(c.sd, rs.Name, requestType, req)
token, err := common.GenerateJWTToken(c.sd, rs.Name, string(requestType), req)
if err != nil {
return nil, err
}
@ -292,8 +401,17 @@ func (c *CommandHandler) HandleRemoteSourceAuth(ctx context.Context, remoteSourc
}
}
type RemoteSourceRequestType string
const (
RemoteSourceRequestTypeCreateUserLA RemoteSourceRequestType = "createuserla"
RemoteSourceRequestTypeLoginUser RemoteSourceRequestType = "loginuser"
RemoteSourceRequestTypeAuthorize RemoteSourceRequestType = "authorize"
RemoteSourceRequestTypeRegisterUser RemoteSourceRequestType = "registeruser"
)
type RemoteSourceAuthResult struct {
RequestType string
RequestType RemoteSourceRequestType
Response interface{}
}
@ -301,9 +419,9 @@ type CreateUserLAResponse struct {
LinkedAccount *types.LinkedAccount
}
func (c *CommandHandler) HandleRemoteSourceAuthRequest(ctx context.Context, requestType, requestString string, userAccessToken, Oauth2AccessToken, Oauth2RefreshToken string) (*RemoteSourceAuthResult, error) {
func (c *CommandHandler) HandleRemoteSourceAuthRequest(ctx context.Context, requestType RemoteSourceRequestType, requestString string, userAccessToken, Oauth2AccessToken, Oauth2RefreshToken string) (*RemoteSourceAuthResult, error) {
switch requestType {
case "createuserla":
case RemoteSourceRequestTypeCreateUserLA:
var req *CreateUserLARequest
if err := json.Unmarshal([]byte(requestString), &req); err != nil {
return nil, errors.Errorf("failed to unmarshal request")
@ -327,7 +445,7 @@ func (c *CommandHandler) HandleRemoteSourceAuthRequest(ctx context.Context, requ
},
}, nil
case "loginuser":
case RemoteSourceRequestTypeLoginUser:
var req *LoginUserRequest
if err := json.Unmarshal([]byte(requestString), &req); err != nil {
return nil, errors.Errorf("failed to unmarshal request")
@ -345,10 +463,50 @@ func (c *CommandHandler) HandleRemoteSourceAuthRequest(ctx context.Context, requ
}
return &RemoteSourceAuthResult{
RequestType: requestType,
Response: &LoginUserResponse{
Token: cresp.Token,
User: cresp.User,
},
Response: cresp,
}, nil
case RemoteSourceRequestTypeAuthorize:
var req *AuthorizeRequest
if err := json.Unmarshal([]byte(requestString), &req); err != nil {
return nil, errors.Errorf("failed to unmarshal request")
}
creq := &AuthorizeRequest{
RemoteSourceName: req.RemoteSourceName,
RemoteSourceUserAccessToken: userAccessToken,
RemoteSourceOauth2AccessToken: Oauth2AccessToken,
RemoteSourceOauth2RefreshToken: Oauth2RefreshToken,
}
cresp, err := c.Authorize(ctx, creq)
if err != nil {
return nil, err
}
return &RemoteSourceAuthResult{
RequestType: requestType,
Response: cresp,
}, nil
case RemoteSourceRequestTypeRegisterUser:
var req *RegisterUserRequest
if err := json.Unmarshal([]byte(requestString), &req); err != nil {
return nil, errors.Errorf("failed to unmarshal request")
}
creq := &RegisterUserRequest{
UserName: req.UserName,
RemoteSourceName: req.RemoteSourceName,
RemoteSourceUserAccessToken: userAccessToken,
RemoteSourceOauth2AccessToken: Oauth2AccessToken,
RemoteSourceOauth2RefreshToken: Oauth2RefreshToken,
}
cresp, err := c.RegisterUser(ctx, creq)
if err != nil {
return nil, err
}
return &RemoteSourceAuthResult{
RequestType: requestType,
Response: cresp,
}, nil
default:
@ -381,7 +539,7 @@ func (c *CommandHandler) HandleOauth2Callback(ctx context.Context, code, state s
claims := token.Claims.(jwt.MapClaims)
remoteSourceName := claims["remote_source_name"].(string)
requestType := claims["request_type"].(string)
requestType := RemoteSourceRequestType(claims["request_type"].(string))
requestString := claims["request"].(string)
rs, _, err := c.configstoreClient.GetRemoteSourceByName(ctx, remoteSourceName)

View File

@ -192,6 +192,8 @@ func (g *Gateway) Run(ctx context.Context) error {
reposHandler := api.NewReposHandler(logger, g.configstoreClient)
loginUserHandler := api.NewLoginUserHandler(logger, g.ch)
authorizeHandler := api.NewAuthorizeHandler(logger, g.ch)
registerHandler := api.NewRegisterUserHandler(logger, g.ch)
oauth2callbackHandler := api.NewOAuth2CallbackHandler(logger, g.ch, g.configstoreClient)
router := mux.NewRouter()
@ -258,6 +260,8 @@ func (g *Gateway) Run(ctx context.Context) error {
apirouter.Handle("/runs", authForcedHandler(runsHandler)).Methods("GET")
router.Handle("/login", loginUserHandler).Methods("POST")
router.Handle("/authorize", authorizeHandler).Methods("POST")
router.Handle("/register", registerHandler).Methods("POST")
router.Handle("/oauth2/callback", oauth2callbackHandler).Methods("GET")
router.Handle("/repos/{rest:.*}", reposHandler).Methods("GET", "POST")