diff --git a/apiclient/types/oauthapp.go b/apiclient/types/oauthapp.go index 95e24018..0e32e54f 100644 --- a/apiclient/types/oauthapp.go +++ b/apiclient/types/oauthapp.go @@ -9,6 +9,7 @@ const ( OAuthAppTypeGitHub OAuthAppType = "github" OAuthAppTypeGoogle OAuthAppType = "google" OAuthAppTypeSalesforce OAuthAppType = "salesforce" + OAuthAppTypeServiceNow OAuthAppType = "servicenow" OAuthAppTypeCustom OAuthAppType = "custom" ) @@ -37,7 +38,7 @@ type OAuthAppManifest struct { Integration string `json:"integration,omitempty"` // Global indicates if the OAuth app is globally applied to all agents. Global *bool `json:"global,omitempty"` - // This field is only used by Salesforce + // This field is only used by Salesforce and ServiceNow InstanceURL string `json:"instanceURL,omitempty"` } diff --git a/pkg/gateway/server/oauth_apps.go b/pkg/gateway/server/oauth_apps.go index 10df9c8f..4cc996f3 100644 --- a/pkg/gateway/server/oauth_apps.go +++ b/pkg/gateway/server/oauth_apps.go @@ -374,7 +374,8 @@ func (s *Server) callbackOAuthApp(apiContext api.Context) error { return fmt.Errorf("failed to create token request: %w", err) } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - if app.Spec.Manifest.Type != types2.OAuthAppTypeGoogle { + if app.Spec.Manifest.Type != types2.OAuthAppTypeGoogle && + app.Spec.Manifest.Type != types2.OAuthAppTypeServiceNow { req.SetBasicAuth(url.QueryEscape(app.Spec.Manifest.ClientID), url.QueryEscape(app.Spec.Manifest.ClientSecret)) } @@ -470,6 +471,25 @@ func (s *Server) callbackOAuthApp(apiContext api.Context) error { "GPTSCRIPT_SALESFORCE_URL": salesforceTokenResp.InstanceURL, }, } + case types2.OAuthAppTypeServiceNow: + serviceNowTokenResp := new(types.ServiceNowOAuthTokenResponse) + if err := json.NewDecoder(resp.Body).Decode(serviceNowTokenResp); err != nil { + return fmt.Errorf("failed to parse token response: %w", err) + } + + tokenResp = &types.OAuthTokenResponse{ + State: state, + TokenType: serviceNowTokenResp.TokenType, + Scope: serviceNowTokenResp.Scope, + AccessToken: serviceNowTokenResp.AccessToken, + ExpiresIn: serviceNowTokenResp.ExpiresIn, + Ok: true, // Assuming true if no error is present + CreatedAt: time.Now(), + RefreshToken: serviceNowTokenResp.RefreshToken, + Extras: map[string]string{ + "GPTSCRIPT_SALESFORCE_URL": app.Spec.Manifest.InstanceURL, + }, + } default: if err := json.NewDecoder(resp.Body).Decode(tokenResp); err != nil { return fmt.Errorf("failed to parse token response: %w", err) diff --git a/pkg/gateway/types/oauth_apps.go b/pkg/gateway/types/oauth_apps.go index 90cda837..18fb635b 100644 --- a/pkg/gateway/types/oauth_apps.go +++ b/pkg/gateway/types/oauth_apps.go @@ -87,6 +87,25 @@ func ValidateAndSetDefaultsOAuthAppManifest(r *types.OAuthAppManifest, create bo if err != nil { errs = append(errs, err) } + case types.OAuthAppTypeServiceNow: + serviceNowAuthorizeFragment := "oauth_auth.do" + serviceNowTokenFragment := "oauth_token.do" + instanceURL, err := url.Parse(r.InstanceURL) + if err != nil { + errs = append(errs, err) + } + if instanceURL.Scheme != "" { + instanceURL.Scheme = "https" + } + + r.AuthURL, err = url.JoinPath(instanceURL.String(), serviceNowAuthorizeFragment) + if err != nil { + errs = append(errs, err) + } + r.TokenURL, err = url.JoinPath(instanceURL.String(), serviceNowTokenFragment) + if err != nil { + errs = append(errs, err) + } } if r.AuthURL == "" { @@ -202,6 +221,14 @@ type SalesforceOAuthTokenResponse struct { IssuedAt string `json:"issued_at"` } +type ServiceNowOAuthTokenResponse struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + Scope string `json:"scope"` + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` +} + type SlackOAuthTokenResponse struct { Ok bool `json:"ok"` Error string `json:"error"` diff --git a/pkg/storage/openapi/generated/openapi_generated.go b/pkg/storage/openapi/generated/openapi_generated.go index c6e98663..18c3055d 100644 --- a/pkg/storage/openapi/generated/openapi_generated.go +++ b/pkg/storage/openapi/generated/openapi_generated.go @@ -2232,7 +2232,7 @@ func schema_obot_platform_obot_apiclient_types_OAuthAppManifest(ref common.Refer }, "instanceURL": { SchemaProps: spec.SchemaProps{ - Description: "This field is only used by Salesforce", + Description: "This field is only used by Salesforce and ServiceNow", Type: []string{"string"}, Format: "", }, diff --git a/ui/admin/app/lib/model/oauthApps/oauth-helpers.ts b/ui/admin/app/lib/model/oauthApps/oauth-helpers.ts index 236588c7..99316f0e 100644 --- a/ui/admin/app/lib/model/oauthApps/oauth-helpers.ts +++ b/ui/admin/app/lib/model/oauthApps/oauth-helpers.ts @@ -9,6 +9,7 @@ export const OAuthProvider = { Microsoft365: "microsoft365", Slack: "slack", Salesforce: "salesforce", + ServiceNow: "servicenow", Notion: "notion", Custom: "custom", } as const; diff --git a/ui/admin/app/lib/model/oauthApps/providers/servicenow.ts b/ui/admin/app/lib/model/oauthApps/providers/servicenow.ts new file mode 100644 index 00000000..60d471f4 --- /dev/null +++ b/ui/admin/app/lib/model/oauthApps/providers/servicenow.ts @@ -0,0 +1,35 @@ +import { z } from "zod"; + +import { + OAuthAppSpec, + OAuthFormStep, + getOAuthLinks, +} from "~/lib/model/oauthApps/oauth-helpers"; + +const schema = z.object({ + clientID: z.string().min(1, "Client ID is required"), + clientSecret: z.string().min(1, "Client Secret is required"), + instanceURL: z.string().min(1, "Instance URL is required"), +}); + +const steps: OAuthFormStep[] = [ + // TODO(njhale): Add instructions for how to set up the OAuth App in ServiceNow and get + // the required values below. + { type: "input", input: "clientID", label: "Consumer Key" }, + { + type: "input", + input: "clientSecret", + label: "Consumer Secret", + inputType: "password", + }, + { type: "input", input: "instanceURL", label: "Instance URL" }, +]; + +export const ServiceNowOAuthApp = { + schema, + alias: "servicenow", + type: "servicenow", + displayName: "ServiceNow", + steps: steps, + noGatewayIntegration: true, +} satisfies OAuthAppSpec;