diff --git a/internal/services/gateway/api/client.go b/internal/services/gateway/api/client.go index 5c4c91f..fc30d93 100644 --- a/internal/services/gateway/api/client.go +++ b/internal/services/gateway/api/client.go @@ -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 { diff --git a/internal/services/gateway/api/oauth2.go b/internal/services/gateway/api/oauth2.go index 3cf6313..9f04b02 100644 --- a/internal/services/gateway/api/oauth2.go +++ b/internal/services/gateway/api/oauth2.go @@ -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 { diff --git a/internal/services/gateway/api/user.go b/internal/services/gateway/api/user.go index 2477627..9622877 100644 --- a/internal/services/gateway/api/user.go +++ b/internal/services/gateway/api/user.go @@ -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 } diff --git a/internal/services/gateway/command/user.go b/internal/services/gateway/command/user.go index 22ae7f4..7ef8ce9 100644 --- a/internal/services/gateway/command/user.go +++ b/internal/services/gateway/command/user.go @@ -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) diff --git a/internal/services/gateway/gateway.go b/internal/services/gateway/gateway.go index ecfafe2..de72c86 100644 --- a/internal/services/gateway/gateway.go +++ b/internal/services/gateway/gateway.go @@ -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")