diff --git a/api-contracts/openapi/openapi.yaml b/api-contracts/openapi/openapi.yaml index 24fcb7596..57980d98d 100644 --- a/api-contracts/openapi/openapi.yaml +++ b/api-contracts/openapi/openapi.yaml @@ -40,6 +40,10 @@ paths: $ref: "./paths/user/user.yaml#/oauth-start-github" /api/v1/users/github/callback: $ref: "./paths/user/user.yaml#/oauth-callback-github" + /api/v1/users/custom/start: + $ref: "./paths/user/user.yaml#/oauth-start-custom" + /api/v1/users/custom/callback: + $ref: "./paths/user/user.yaml#/oauth-callback-custom" /api/v1/tenants/{tenant}/slack/start: $ref: "./paths/user/user.yaml#/oauth-start-slack" /api/v1/users/slack/callback: diff --git a/api-contracts/openapi/paths/user/user.yaml b/api-contracts/openapi/paths/user/user.yaml index bbb8171ed..49d663d8a 100644 --- a/api-contracts/openapi/paths/user/user.yaml +++ b/api-contracts/openapi/paths/user/user.yaml @@ -177,6 +177,36 @@ oauth-callback-google: summary: Complete OAuth flow tags: - User +oauth-start-custom: + get: + description: Starts the OAuth flow + operationId: user:update:custom-oauth-start + responses: + "302": + description: Successfully started the OAuth flow + headers: + location: + schema: + type: string + security: [] + summary: Start OAuth flow + tags: + - User +oauth-callback-custom: + get: + description: Completes the OAuth flow + operationId: user:update:custom-oauth-callback + responses: + "302": + description: Successfully completed the OAuth flow + headers: + location: + schema: + type: string + security: [] + summary: Complete OAuth flow + tags: + - User oauth-start-github: get: description: Starts the OAuth flow diff --git a/api/v1/server/handlers/metadata/get.go b/api/v1/server/handlers/metadata/get.go index aa81ee17d..a0dd40bdc 100644 --- a/api/v1/server/handlers/metadata/get.go +++ b/api/v1/server/handlers/metadata/get.go @@ -21,6 +21,10 @@ func (u *MetadataService) MetadataGet(ctx echo.Context, request gen.MetadataGetR authTypes = append(authTypes, "github") } + if u.config.Auth.ConfigFile.Custom.Enabled { + authTypes = append(authTypes, "custom") + } + pylonAppID := u.config.Pylon.AppID var posthogConfig *gen.APIMetaPosthog diff --git a/api/v1/server/handlers/users/custom_oauth_callback.go b/api/v1/server/handlers/users/custom_oauth_callback.go new file mode 100644 index 000000000..d999ea118 --- /dev/null +++ b/api/v1/server/handlers/users/custom_oauth_callback.go @@ -0,0 +1,171 @@ +package users + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + + "github.com/labstack/echo/v4" + "golang.org/x/oauth2" + + "github.com/hatchet-dev/hatchet/api/v1/server/authn" + "github.com/hatchet-dev/hatchet/api/v1/server/middleware/redirect" + "github.com/hatchet-dev/hatchet/api/v1/server/oas/gen" + "github.com/hatchet-dev/hatchet/pkg/config/server" + "github.com/hatchet-dev/hatchet/pkg/repository" + "github.com/hatchet-dev/hatchet/pkg/repository/prisma/db" +) + +// Note: we want all errors to redirect, otherwise the user will be greeted with raw JSON in the middle of the login flow. +func (u *UserService) UserUpdateCustomOauthCallback(ctx echo.Context, _ gen.UserUpdateCustomOauthCallbackRequestObject) (gen.UserUpdateCustomOauthCallbackResponseObject, error) { + isValid, _, err := authn.NewSessionHelpers(u.config).ValidateOAuthState(ctx, "custom") + + if err != nil || !isValid { + return nil, redirect.GetRedirectWithError(ctx, u.config.Logger, err, "Could not log in. Please try again and make sure cookies are enabled.") + } + + token, err := u.config.Auth.CustomOAuthConfig.Exchange(context.Background(), ctx.Request().URL.Query().Get("code")) + + if err != nil { + return nil, redirect.GetRedirectWithError(ctx, u.config.Logger, err, "Forbidden") + } + + if !token.Valid() { + return nil, redirect.GetRedirectWithError(ctx, u.config.Logger, fmt.Errorf("invalid token"), "Forbidden") + } + + user, err := u.upsertCustomUserFromToken(u.config, token) + + if err != nil { + if errors.Is(err, ErrNotInRestrictedDomain) { + return nil, redirect.GetRedirectWithError(ctx, u.config.Logger, err, "Email is not in the restricted domain group.") + } + + return nil, redirect.GetRedirectWithError(ctx, u.config.Logger, err, "Internal error.") + } + + err = authn.NewSessionHelpers(u.config).SaveAuthenticated(ctx, user) + + if err != nil { + return nil, redirect.GetRedirectWithError(ctx, u.config.Logger, err, "Internal error.") + } + + return gen.UserUpdateCustomOauthCallback302Response{ + Headers: gen.UserUpdateCustomOauthCallback302ResponseHeaders{ + Location: u.config.Runtime.ServerURL, + }, + }, nil +} + +func (u *UserService) upsertCustomUserFromToken(config *server.ServerConfig, tok *oauth2.Token) (*db.UserModel, error) { + cInfo, err := getCustomUserInfoFromToken(config, tok) + if err != nil { + return nil, err + } + + if err := u.checkUserRestrictions(config, cInfo.Email); err != nil { + return nil, err + } + + expiresAt := tok.Expiry + + // use the encryption service to encrypt the access and refresh token + accessTokenEncrypted, err := config.Encryption.Encrypt([]byte(tok.AccessToken), "custom_access_token") + + if err != nil { + return nil, fmt.Errorf("failed to encrypt access token: %s", err.Error()) + } + + refreshTokenEncrypted, err := config.Encryption.Encrypt([]byte(tok.RefreshToken), "custom_refresh_token") + + if err != nil { + return nil, fmt.Errorf("failed to encrypt refresh token: %s", err.Error()) + } + + oauthOpts := &repository.OAuthOpts{ + Provider: "custom", + ProviderUserId: cInfo.Sub, + AccessToken: accessTokenEncrypted, + RefreshToken: &refreshTokenEncrypted, + ExpiresAt: &expiresAt, + } + + user, err := u.config.APIRepository.User().GetUserByEmail(cInfo.Email) + + switch err { + case nil: + user, err = u.config.APIRepository.User().UpdateUser(user.ID, &repository.UpdateUserOpts{ + EmailVerified: repository.BoolPtr(cInfo.EmailVerified), + Name: repository.StringPtr(cInfo.Name), + OAuth: oauthOpts, + }) + + if err != nil { + return nil, fmt.Errorf("failed to update user: %s", err.Error()) + } + case db.ErrNotFound: + user, err = u.config.APIRepository.User().CreateUser(&repository.CreateUserOpts{ + Email: cInfo.Email, + EmailVerified: repository.BoolPtr(cInfo.EmailVerified), + Name: repository.StringPtr(cInfo.Name), + OAuth: oauthOpts, + }) + + if err != nil { + return nil, fmt.Errorf("failed to create user: %s", err.Error()) + } + default: + return nil, fmt.Errorf("failed to get user: %s", err.Error()) + } + + return user, nil +} + +type customUserInfo struct { + Sub string `json:"sub"` + Email string `json:"email"` + EmailVerified bool `json:"email_verified"` + Name string `json:"name"` +} + +func getCustomUserInfoFromToken(config *server.ServerConfig, tok *oauth2.Token) (*customUserInfo, error) { + // use ResourceURL endpoint from the config + url := config.Auth.ConfigFile.Custom.ResourceURL + + fmt.Printf("Response body contents: %s", config.Auth.ConfigFile.Custom.Scopes) + + req, err := http.NewRequest("GET", url, nil) + + if err != nil { + return nil, fmt.Errorf("failed creating request: %s", err.Error()) + } + + req.Header.Add("Authorization", "Bearer "+tok.AccessToken) + + client := &http.Client{} + + response, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed getting user info: %s", err.Error()) + } + + defer response.Body.Close() + + contents, err := io.ReadAll(response.Body) + if err != nil { + return nil, fmt.Errorf("failed reading response body: %s", err.Error()) + } + + // parse contents into generic oauth2 userinfo claims + cInfo := &customUserInfo{} + err = json.Unmarshal(contents, &cInfo) + + if err != nil { + return nil, fmt.Errorf("failed parsing response body: %s", err.Error()) + } + + return cInfo, nil +} diff --git a/api/v1/server/handlers/users/custom_oauth_start.go b/api/v1/server/handlers/users/custom_oauth_start.go new file mode 100644 index 000000000..85b1cdd4d --- /dev/null +++ b/api/v1/server/handlers/users/custom_oauth_start.go @@ -0,0 +1,30 @@ +package users + +import ( + "github.com/labstack/echo/v4" + + "github.com/hatchet-dev/hatchet/api/v1/server/authn" + "github.com/hatchet-dev/hatchet/api/v1/server/middleware/redirect" + "github.com/hatchet-dev/hatchet/api/v1/server/oas/gen" +) + +// Note: we want all errors to redirect, otherwise the user will be greeted with raw JSON in the middle of the login flow. +func (u *UserService) UserUpdateCustomOauthStart(ctx echo.Context, _ gen.UserUpdateCustomOauthStartRequestObject) (gen.UserUpdateCustomOauthStartResponseObject, error) { + if !u.config.Runtime.AllowSignup { + return nil, redirect.GetRedirectWithError(ctx, u.config.Logger, nil, "User signup is disabled.") + } + + state, err := authn.NewSessionHelpers(u.config).SaveOAuthState(ctx, "custom") + + if err != nil { + return nil, redirect.GetRedirectWithError(ctx, u.config.Logger, err, "Could not get cookie. Please make sure cookies are enabled.") + } + + url := u.config.Auth.CustomOAuthConfig.AuthCodeURL(state) + + return gen.UserUpdateCustomOauthStart302Response{ + Headers: gen.UserUpdateCustomOauthStart302ResponseHeaders{ + Location: url, + }, + }, nil +} diff --git a/api/v1/server/oas/gen/openapi.gen.go b/api/v1/server/oas/gen/openapi.gen.go index c890c55d0..2e6ea1b7c 100644 --- a/api/v1/server/oas/gen/openapi.gen.go +++ b/api/v1/server/oas/gen/openapi.gen.go @@ -2023,6 +2023,12 @@ type ServerInterface interface { // (GET /api/v1/users/current) UserGetCurrent(ctx echo.Context) error // Complete OAuth flow + // (GET /api/v1/users/custom/callback) + UserUpdateCustomOauthCallback(ctx echo.Context) error + // Start OAuth flow + // (GET /api/v1/users/custom/start) + UserUpdateCustomOauthStart(ctx echo.Context) error + // Complete OAuth flow // (GET /api/v1/users/github/callback) UserUpdateGithubOauthCallback(ctx echo.Context) error // Start OAuth flow @@ -4077,6 +4083,24 @@ func (w *ServerInterfaceWrapper) UserGetCurrent(ctx echo.Context) error { return err } +// UserUpdateCustomOauthCallback converts echo context to params. +func (w *ServerInterfaceWrapper) UserUpdateCustomOauthCallback(ctx echo.Context) error { + var err error + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.UserUpdateCustomOauthCallback(ctx) + return err +} + +// UserUpdateCustomOauthStart converts echo context to params. +func (w *ServerInterfaceWrapper) UserUpdateCustomOauthStart(ctx echo.Context) error { + var err error + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.UserUpdateCustomOauthStart(ctx) + return err +} + // UserUpdateGithubOauthCallback converts echo context to params. func (w *ServerInterfaceWrapper) UserUpdateGithubOauthCallback(ctx echo.Context) error { var err error @@ -4544,6 +4568,8 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL router.POST(baseURL+"/api/v1/tenants/:tenant/workflows/:workflow/scheduled", wrapper.ScheduledWorkflowRunCreate) router.GET(baseURL+"/api/v1/tenants/:tenant/workflows/:workflow/worker-count", wrapper.WorkflowGetWorkersCount) router.GET(baseURL+"/api/v1/users/current", wrapper.UserGetCurrent) + router.GET(baseURL+"/api/v1/users/custom/callback", wrapper.UserUpdateCustomOauthCallback) + router.GET(baseURL+"/api/v1/users/custom/start", wrapper.UserUpdateCustomOauthStart) router.GET(baseURL+"/api/v1/users/github/callback", wrapper.UserUpdateGithubOauthCallback) router.GET(baseURL+"/api/v1/users/github/start", wrapper.UserUpdateGithubOauthStart) router.GET(baseURL+"/api/v1/users/google/callback", wrapper.UserUpdateGoogleOauthCallback) @@ -7230,6 +7256,48 @@ func (response UserGetCurrent405JSONResponse) VisitUserGetCurrentResponse(w http return json.NewEncoder(w).Encode(response) } +type UserUpdateCustomOauthCallbackRequestObject struct { +} + +type UserUpdateCustomOauthCallbackResponseObject interface { + VisitUserUpdateCustomOauthCallbackResponse(w http.ResponseWriter) error +} + +type UserUpdateCustomOauthCallback302ResponseHeaders struct { + Location string +} + +type UserUpdateCustomOauthCallback302Response struct { + Headers UserUpdateCustomOauthCallback302ResponseHeaders +} + +func (response UserUpdateCustomOauthCallback302Response) VisitUserUpdateCustomOauthCallbackResponse(w http.ResponseWriter) error { + w.Header().Set("location", fmt.Sprint(response.Headers.Location)) + w.WriteHeader(302) + return nil +} + +type UserUpdateCustomOauthStartRequestObject struct { +} + +type UserUpdateCustomOauthStartResponseObject interface { + VisitUserUpdateCustomOauthStartResponse(w http.ResponseWriter) error +} + +type UserUpdateCustomOauthStart302ResponseHeaders struct { + Location string +} + +type UserUpdateCustomOauthStart302Response struct { + Headers UserUpdateCustomOauthStart302ResponseHeaders +} + +func (response UserUpdateCustomOauthStart302Response) VisitUserUpdateCustomOauthStartResponse(w http.ResponseWriter) error { + w.Header().Set("location", fmt.Sprint(response.Headers.Location)) + w.WriteHeader(302) + return nil +} + type UserUpdateGithubOauthCallbackRequestObject struct { } @@ -8213,6 +8281,10 @@ type StrictServerInterface interface { UserGetCurrent(ctx echo.Context, request UserGetCurrentRequestObject) (UserGetCurrentResponseObject, error) + UserUpdateCustomOauthCallback(ctx echo.Context, request UserUpdateCustomOauthCallbackRequestObject) (UserUpdateCustomOauthCallbackResponseObject, error) + + UserUpdateCustomOauthStart(ctx echo.Context, request UserUpdateCustomOauthStartRequestObject) (UserUpdateCustomOauthStartResponseObject, error) + UserUpdateGithubOauthCallback(ctx echo.Context, request UserUpdateGithubOauthCallbackRequestObject) (UserUpdateGithubOauthCallbackResponseObject, error) UserUpdateGithubOauthStart(ctx echo.Context, request UserUpdateGithubOauthStartRequestObject) (UserUpdateGithubOauthStartResponseObject, error) @@ -10146,6 +10218,52 @@ func (sh *strictHandler) UserGetCurrent(ctx echo.Context) error { return nil } +// UserUpdateCustomOauthCallback operation middleware +func (sh *strictHandler) UserUpdateCustomOauthCallback(ctx echo.Context) error { + var request UserUpdateCustomOauthCallbackRequestObject + + handler := func(ctx echo.Context, request interface{}) (interface{}, error) { + return sh.ssi.UserUpdateCustomOauthCallback(ctx, request.(UserUpdateCustomOauthCallbackRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "UserUpdateCustomOauthCallback") + } + + response, err := handler(ctx, request) + + if err != nil { + return err + } else if validResponse, ok := response.(UserUpdateCustomOauthCallbackResponseObject); ok { + return validResponse.VisitUserUpdateCustomOauthCallbackResponse(ctx.Response()) + } else if response != nil { + return fmt.Errorf("Unexpected response type: %T", response) + } + return nil +} + +// UserUpdateCustomOauthStart operation middleware +func (sh *strictHandler) UserUpdateCustomOauthStart(ctx echo.Context) error { + var request UserUpdateCustomOauthStartRequestObject + + handler := func(ctx echo.Context, request interface{}) (interface{}, error) { + return sh.ssi.UserUpdateCustomOauthStart(ctx, request.(UserUpdateCustomOauthStartRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "UserUpdateCustomOauthStart") + } + + response, err := handler(ctx, request) + + if err != nil { + return err + } else if validResponse, ok := response.(UserUpdateCustomOauthStartResponseObject); ok { + return validResponse.VisitUserUpdateCustomOauthStartResponse(ctx.Response()) + } else if response != nil { + return fmt.Errorf("Unexpected response type: %T", response) + } + return nil +} + // UserUpdateGithubOauthCallback operation middleware func (sh *strictHandler) UserUpdateGithubOauthCallback(ctx echo.Context) error { var request UserUpdateGithubOauthCallbackRequestObject @@ -10749,203 +10867,203 @@ func (sh *strictHandler) WorkflowVersionGet(ctx echo.Context, workflow openapi_t // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/frontend/app/src/lib/api/generated/Api.ts b/frontend/app/src/lib/api/generated/Api.ts index 7535e4778..ace31e4e8 100644 --- a/frontend/app/src/lib/api/generated/Api.ts +++ b/frontend/app/src/lib/api/generated/Api.ts @@ -258,6 +258,34 @@ export class Api extends HttpClient + this.request({ + path: `/api/v1/users/custom/start`, + method: 'GET', + ...params, + }); + /** + * @description Completes the OAuth flow + * + * @tags User + * @name UserUpdateCustomOauthCallback + * @summary Complete OAuth flow + * @request GET:/api/v1/users/custom/callback + */ + userUpdateCustomOauthCallback = (params: RequestParams = {}) => + this.request({ + path: `/api/v1/users/custom/callback`, + method: 'GET', + ...params, + }); /** * @description Starts the OAuth flow * diff --git a/frontend/app/src/pages/auth/login/index.tsx b/frontend/app/src/pages/auth/login/index.tsx index bdd5c94fc..9ba2bed44 100644 --- a/frontend/app/src/pages/auth/login/index.tsx +++ b/frontend/app/src/pages/auth/login/index.tsx @@ -23,13 +23,14 @@ export default function Login() { const basicEnabled = schemes.includes('basic'); const googleEnabled = schemes.includes('google'); const githubEnabled = schemes.includes('github'); + const customEnabled = schemes.includes('custom'); let prompt = 'Enter your email and password below.'; - if (basicEnabled && (googleEnabled || githubEnabled)) { + if (basicEnabled && (googleEnabled || githubEnabled || customEnabled)) { prompt = 'Enter your email and password below, or continue with a supported provider.'; - } else if (googleEnabled || githubEnabled) { + } else if (googleEnabled || githubEnabled || customEnabled) { prompt = 'Continue with a supported provider.'; } else if (basicEnabled) { prompt = 'Enter your email and password below.'; @@ -41,6 +42,7 @@ export default function Login() { basicEnabled && , googleEnabled && , githubEnabled && , + customEnabled && , ].filter(Boolean); return ( @@ -156,6 +158,16 @@ export function GoogleLogin() { ); } +export function CustomLogin() { + return ( + + + + ); +} + export function GithubLogin() { return ( diff --git a/internal/services/admin/contracts/workflows.pb.go b/internal/services/admin/contracts/workflows.pb.go index 07afa2190..a2e793f92 100644 --- a/internal/services/admin/contracts/workflows.pb.go +++ b/internal/services/admin/contracts/workflows.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.28.1 -// protoc v5.28.2 +// protoc v5.29.1 // source: workflows.proto package contracts diff --git a/internal/services/admin/contracts/workflows_grpc.pb.go b/internal/services/admin/contracts/workflows_grpc.pb.go index 540001dc2..2e3beb62c 100644 --- a/internal/services/admin/contracts/workflows_grpc.pb.go +++ b/internal/services/admin/contracts/workflows_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.2.0 -// - protoc v5.28.2 +// - protoc v5.29.1 // source: workflows.proto package contracts diff --git a/internal/services/dispatcher/contracts/dispatcher.pb.go b/internal/services/dispatcher/contracts/dispatcher.pb.go index 85e91701a..1d29d62a5 100644 --- a/internal/services/dispatcher/contracts/dispatcher.pb.go +++ b/internal/services/dispatcher/contracts/dispatcher.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.28.1 -// protoc v5.28.2 +// protoc v5.29.1 // source: dispatcher.proto package contracts diff --git a/internal/services/dispatcher/contracts/dispatcher_grpc.pb.go b/internal/services/dispatcher/contracts/dispatcher_grpc.pb.go index 220364f55..bc46fdab1 100644 --- a/internal/services/dispatcher/contracts/dispatcher_grpc.pb.go +++ b/internal/services/dispatcher/contracts/dispatcher_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.2.0 -// - protoc v5.28.2 +// - protoc v5.29.1 // source: dispatcher.proto package contracts diff --git a/internal/services/ingestor/contracts/events.pb.go b/internal/services/ingestor/contracts/events.pb.go index ce923eb1a..aafa0988d 100644 --- a/internal/services/ingestor/contracts/events.pb.go +++ b/internal/services/ingestor/contracts/events.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.28.1 -// protoc v5.28.2 +// protoc v5.29.1 // source: events.proto package contracts diff --git a/internal/services/ingestor/contracts/events_grpc.pb.go b/internal/services/ingestor/contracts/events_grpc.pb.go index f3af5090d..bcb14a767 100644 --- a/internal/services/ingestor/contracts/events_grpc.pb.go +++ b/internal/services/ingestor/contracts/events_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.2.0 -// - protoc v5.28.2 +// - protoc v5.29.1 // source: events.proto package contracts diff --git a/pkg/auth/oauth/configs.go b/pkg/auth/oauth/configs.go index a0d181238..57166e954 100644 --- a/pkg/auth/oauth/configs.go +++ b/pkg/auth/oauth/configs.go @@ -11,6 +11,11 @@ type Config struct { BaseURL string } +type CustomConfig struct { + AuthURL string + TokenURL string +} + const ( GoogleAuthURL string = "https://accounts.google.com/o/oauth2/v2/auth" GoogleTokenURL string = "https://oauth2.googleapis.com/token" // #nosec G101 @@ -58,3 +63,16 @@ func NewSlackClient(cfg *Config) *oauth2.Config { Scopes: cfg.Scopes, } } + +func NewCustomClient(cfg *Config, cfe *CustomConfig) *oauth2.Config { + return &oauth2.Config{ + ClientID: cfg.ClientID, + ClientSecret: cfg.ClientSecret, + RedirectURL: cfg.BaseURL + "/api/v1/users/custom/callback", + Endpoint: oauth2.Endpoint{ + AuthURL: cfe.AuthURL, + TokenURL: cfe.TokenURL, + }, + Scopes: cfg.Scopes, + } +} diff --git a/pkg/client/rest/gen.go b/pkg/client/rest/gen.go index c670e9c08..b14c4cad1 100644 --- a/pkg/client/rest/gen.go +++ b/pkg/client/rest/gen.go @@ -2126,6 +2126,12 @@ type ClientInterface interface { // UserGetCurrent request UserGetCurrent(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + // UserUpdateCustomOauthCallback request + UserUpdateCustomOauthCallback(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + + // UserUpdateCustomOauthStart request + UserUpdateCustomOauthStart(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + // UserUpdateGithubOauthCallback request UserUpdateGithubOauthCallback(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -3268,6 +3274,30 @@ func (c *Client) UserGetCurrent(ctx context.Context, reqEditors ...RequestEditor return c.Client.Do(req) } +func (c *Client) UserUpdateCustomOauthCallback(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewUserUpdateCustomOauthCallbackRequest(c.Server) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) UserUpdateCustomOauthStart(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewUserUpdateCustomOauthStartRequest(c.Server) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) UserUpdateGithubOauthCallback(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewUserUpdateGithubOauthCallbackRequest(c.Server) if err != nil { @@ -7498,6 +7528,60 @@ func NewUserGetCurrentRequest(server string) (*http.Request, error) { return req, nil } +// NewUserUpdateCustomOauthCallbackRequest generates requests for UserUpdateCustomOauthCallback +func NewUserUpdateCustomOauthCallbackRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/api/v1/users/custom/callback") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewUserUpdateCustomOauthStartRequest generates requests for UserUpdateCustomOauthStart +func NewUserUpdateCustomOauthStartRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/api/v1/users/custom/start") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + // NewUserUpdateGithubOauthCallbackRequest generates requests for UserUpdateGithubOauthCallback func NewUserUpdateGithubOauthCallbackRequest(server string) (*http.Request, error) { var err error @@ -8664,6 +8748,12 @@ type ClientWithResponsesInterface interface { // UserGetCurrentWithResponse request UserGetCurrentWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*UserGetCurrentResponse, error) + // UserUpdateCustomOauthCallbackWithResponse request + UserUpdateCustomOauthCallbackWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*UserUpdateCustomOauthCallbackResponse, error) + + // UserUpdateCustomOauthStartWithResponse request + UserUpdateCustomOauthStartWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*UserUpdateCustomOauthStartResponse, error) + // UserUpdateGithubOauthCallbackWithResponse request UserUpdateGithubOauthCallbackWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*UserUpdateGithubOauthCallbackResponse, error) @@ -10440,6 +10530,48 @@ func (r UserGetCurrentResponse) StatusCode() int { return 0 } +type UserUpdateCustomOauthCallbackResponse struct { + Body []byte + HTTPResponse *http.Response +} + +// Status returns HTTPResponse.Status +func (r UserUpdateCustomOauthCallbackResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r UserUpdateCustomOauthCallbackResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type UserUpdateCustomOauthStartResponse struct { + Body []byte + HTTPResponse *http.Response +} + +// Status returns HTTPResponse.Status +func (r UserUpdateCustomOauthStartResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r UserUpdateCustomOauthStartResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type UserUpdateGithubOauthCallbackResponse struct { Body []byte HTTPResponse *http.Response @@ -11759,6 +11891,24 @@ func (c *ClientWithResponses) UserGetCurrentWithResponse(ctx context.Context, re return ParseUserGetCurrentResponse(rsp) } +// UserUpdateCustomOauthCallbackWithResponse request returning *UserUpdateCustomOauthCallbackResponse +func (c *ClientWithResponses) UserUpdateCustomOauthCallbackWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*UserUpdateCustomOauthCallbackResponse, error) { + rsp, err := c.UserUpdateCustomOauthCallback(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseUserUpdateCustomOauthCallbackResponse(rsp) +} + +// UserUpdateCustomOauthStartWithResponse request returning *UserUpdateCustomOauthStartResponse +func (c *ClientWithResponses) UserUpdateCustomOauthStartWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*UserUpdateCustomOauthStartResponse, error) { + rsp, err := c.UserUpdateCustomOauthStart(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseUserUpdateCustomOauthStartResponse(rsp) +} + // UserUpdateGithubOauthCallbackWithResponse request returning *UserUpdateGithubOauthCallbackResponse func (c *ClientWithResponses) UserUpdateGithubOauthCallbackWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*UserUpdateGithubOauthCallbackResponse, error) { rsp, err := c.UserUpdateGithubOauthCallback(ctx, reqEditors...) @@ -14867,6 +15017,38 @@ func ParseUserGetCurrentResponse(rsp *http.Response) (*UserGetCurrentResponse, e return response, nil } +// ParseUserUpdateCustomOauthCallbackResponse parses an HTTP response from a UserUpdateCustomOauthCallbackWithResponse call +func ParseUserUpdateCustomOauthCallbackResponse(rsp *http.Response) (*UserUpdateCustomOauthCallbackResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &UserUpdateCustomOauthCallbackResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + return response, nil +} + +// ParseUserUpdateCustomOauthStartResponse parses an HTTP response from a UserUpdateCustomOauthStartWithResponse call +func ParseUserUpdateCustomOauthStartResponse(rsp *http.Response) (*UserUpdateCustomOauthStartResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &UserUpdateCustomOauthStartResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + return response, nil +} + // ParseUserUpdateGithubOauthCallbackResponse parses an HTTP response from a UserUpdateGithubOauthCallbackWithResponse call func ParseUserUpdateGithubOauthCallbackResponse(rsp *http.Response) (*UserUpdateGithubOauthCallbackResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) diff --git a/pkg/config/loader/loader.go b/pkg/config/loader/loader.go index ac8bc1aa6..2a7a616b3 100644 --- a/pkg/config/loader/loader.go +++ b/pkg/config/loader/loader.go @@ -412,6 +412,34 @@ func GetServerConfigFromConfigfile(dc *database.Config, cf *server.ServerConfigF }) } + if cf.Auth.Custom.Enabled { + if cf.Auth.Custom.ClientID == "" { + return nil, nil, fmt.Errorf("github client id is required") + } + + if cf.Auth.Custom.ClientSecret == "" { + return nil, nil, fmt.Errorf("github client secret is required") + } + + if cf.Auth.Custom.AuthorizationURL == "" { + return nil, nil, fmt.Errorf("custom authorization url is required") + } + + if cf.Auth.Custom.AccessTokenURL == "" { + return nil, nil, fmt.Errorf("custom token url is required") + } + + auth.CustomOAuthConfig = oauth.NewCustomClient(&oauth.Config{ + ClientID: cf.Auth.Custom.ClientID, + ClientSecret: cf.Auth.Custom.ClientSecret, + BaseURL: cf.Runtime.ServerURL, + Scopes: cf.Auth.Custom.Scopes, + }, &oauth.CustomConfig{ + AuthURL: cf.Auth.Custom.AuthorizationURL, + TokenURL: cf.Auth.Custom.AccessTokenURL, + }) + } + encryptionSvc, err := loadEncryptionSvc(cf) if err != nil { diff --git a/pkg/config/server/server.go b/pkg/config/server/server.go index 73e709fd0..bc0e11959 100644 --- a/pkg/config/server/server.go +++ b/pkg/config/server/server.go @@ -298,6 +298,8 @@ type ConfigFileAuth struct { Google ConfigFileAuthGoogle `mapstructure:"google" json:"google,omitempty"` Github ConfigFileAuthGithub `mapstructure:"github" json:"github,omitempty"` + + Custom ConfigFileAuthCustom `mapstructure:"custom" json:"custom,omitempty"` } type ConfigFileTenantAlerting struct { @@ -328,6 +330,21 @@ type ConfigFileAuthGithub struct { Scopes []string `mapstructure:"scopes" json:"scopes,omitempty" default:"[\"read:user\", \"user:email\"]"` } +type ConfigFileAuthCustom struct { + Enabled bool `mapstructure:"enabled" json:"enabled,omitempty" default:"false"` + + ClientID string `mapstructure:"clientID" json:"clientID,omitempty"` + ClientSecret string `mapstructure:"clientSecret" json:"clientSecret,omitempty"` + + AuthorizationURL string `mapstructure:"authorizationURL" json:"authorizationURL,omitempty"` + AccessTokenURL string `mapstructure:"accessTokenURL" json:"accessTokenURL,omitempty"` + ResourceURL string `mapstructure:"resourceURL" json:"resourceURL,omitempty"` + RedirectURL string `mapstructure:"redirectURL" json:"redirectURL,omitempty"` + UserIdentifier string `mapstructure:"userIdentifier" json:"userIdentifier,omitempty"` + + Scopes []string `mapstructure:"scopes" json:"scopes,omitempty"` +} + type ConfigFileAuthCookie struct { Name string `mapstructure:"name" json:"name,omitempty" default:"hatchet"` Domain string `mapstructure:"domain" json:"domain,omitempty"` @@ -390,6 +407,8 @@ type AuthConfig struct { GithubOAuthConfig *oauth2.Config + CustomOAuthConfig *oauth2.Config + JWTManager token.JWTManager } @@ -588,6 +607,13 @@ func BindAllEnv(v *viper.Viper) { _ = v.BindEnv("auth.github.clientID", "SERVER_AUTH_GITHUB_CLIENT_ID") _ = v.BindEnv("auth.github.clientSecret", "SERVER_AUTH_GITHUB_CLIENT_SECRET") _ = v.BindEnv("auth.github.scopes", "SERVER_AUTH_GITHUB_SCOPES") + _ = v.BindEnv("auth.custom.enabled", "SERVER_AUTH_CUSTOM_ENABLED") + _ = v.BindEnv("auth.custom.clientID", "SERVER_AUTH_CUSTOM_CLIENT_ID") + _ = v.BindEnv("auth.custom.clientSecret", "SERVER_AUTH_CUSTOM_CLIENT_SECRET") + _ = v.BindEnv("auth.custom.authorizationURL", "SERVER_AUTH_CUSTOM_AUTHORIZATION_URL") + _ = v.BindEnv("auth.custom.accessTokenURL", "SERVER_AUTH_CUSTOM_ACCESS_TOKEN_URL") + _ = v.BindEnv("auth.custom.resourceURL", "SERVER_AUTH_CUSTOM_RESOURCE_URL") + _ = v.BindEnv("auth.custom.scopes", "SERVER_AUTH_CUSTOM_SCOPES") // task queue options // legacy options diff --git a/pkg/repository/user.go b/pkg/repository/user.go index 28ce2810a..b56cbbb67 100644 --- a/pkg/repository/user.go +++ b/pkg/repository/user.go @@ -20,7 +20,7 @@ type CreateUserOpts struct { } type OAuthOpts struct { - Provider string `validate:"required,oneof=google github"` + Provider string `validate:"required,oneof=google github custom"` ProviderUserId string `validate:"required,min=1"` AccessToken []byte `validate:"required,min=1"` RefreshToken *[]byte // optional diff --git a/sql/migrations/20241219162253_temp.sql b/sql/migrations/20241219162253_temp.sql new file mode 100644 index 000000000..dbce59331 --- /dev/null +++ b/sql/migrations/20241219162253_temp.sql @@ -0,0 +1,20 @@ +-- Modify "LogLine" table +ALTER TABLE "LogLine" DROP CONSTRAINT "LogLine_stepRunId_fkey"; +-- Drop index "StepRun_id_key" from table: "StepRun" +DROP INDEX "StepRun_id_key"; +-- Modify "StepRun" table +ALTER TABLE "StepRun" DROP CONSTRAINT "StepRun_jobRunId_fkey", DROP CONSTRAINT "StepRun_workerId_fkey"; +-- Create index "StepRun_id_key" to table: "StepRun" +CREATE UNIQUE INDEX "StepRun_id_key" ON "StepRun" ("id", "status"); +-- Create index "StepRun_status_tenantId_idx" to table: "StepRun" +CREATE INDEX "StepRun_status_tenantId_idx" ON "StepRun" ("status", "tenantId"); +-- Modify "StepRunResultArchive" table +ALTER TABLE "StepRunResultArchive" DROP CONSTRAINT "StepRunResultArchive_stepRunId_fkey"; +-- Modify "StreamEvent" table +ALTER TABLE "StreamEvent" DROP CONSTRAINT "StreamEvent_stepRunId_fkey"; +-- Modify "WorkflowRun" table +ALTER TABLE "WorkflowRun" DROP CONSTRAINT "WorkflowRun_parentStepRunId_fkey"; +-- Modify "WorkflowTriggerScheduledRef" table +ALTER TABLE "WorkflowTriggerScheduledRef" DROP CONSTRAINT "WorkflowTriggerScheduledRef_parentStepRunId_fkey"; +-- Modify "_StepRunOrder" table +ALTER TABLE "_StepRunOrder" DROP CONSTRAINT "_StepRunOrder_A_fkey", DROP CONSTRAINT "_StepRunOrder_B_fkey"; diff --git a/sql/migrations/atlas.sum b/sql/migrations/atlas.sum index 3c857910d..f6d415a3b 100644 --- a/sql/migrations/atlas.sum +++ b/sql/migrations/atlas.sum @@ -1,4 +1,4 @@ -h1:1Az5U4thlaLVJj4xo1BN9WtRVjaMytq41j5vy94dyuE= +h1:u6M95B1VkHecY6gFeInof9wDKsOdJHAFxpMlPsTC8uE= 20240115180414_init.sql h1:Ef3ZyjAHkmJPdGF/dEWCahbwgcg6uGJKnDxW2JCRi2k= 20240122014727_v0_6_0.sql h1:o/LdlteAeFgoHJ3e/M4Xnghqt9826IE/Y/h0q95Acuo= 20240126235456_v0_7_0.sql h1:KiVzt/hXgQ6esbdC6OMJOOWuYEXmy1yeCpmsVAHTFKs= @@ -80,3 +80,4 @@ h1:1Az5U4thlaLVJj4xo1BN9WtRVjaMytq41j5vy94dyuE= 20241206231312_v0.52.12.sql h1:6L/zXbiVC24nqSzJzqItPFKCA3HPyMk0T5pBPnmXQgg= 20241216175807_v0.52.13.sql h1:rMwIaYvy3WX/F7/go1J3vI+WNYnABpASv0ATPJt1pE8= 20241217152316_v0.53.0.sql h1:iFz58oq8r6rDcM3HcainoblLXwOpCgayvNdQwC77Sho= +20241219162253_temp.sql h1:/ymHdjUDKjlcBOk5n/1cYPGC/asC67XbHjmJFeLU67Q=