Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(integration): identify supported OAuth integrations through global secrets #791

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 16 additions & 10 deletions .env.component
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,24 @@

# Provide your API key for the AI vendors so that you can set the components up
# with default credentials.
CFG_CONNECTOR_SECRETS_OPENAI_APIKEY=
CFG_CONNECTOR_SECRETS_STABILITYAI_APIKEY=
CFG_CONNECTOR_SECRETS_ANTHROPIC_APIKEY=
CFG_CONNECTOR_SECRETS_COHERE_APIKEY=
CFG_CONNECTOR_SECRETS_MISTRALAI_APIKEY=
CFG_CONNECTOR_SECRETS_GROQ_APIKEY=
CFG_CONNECTOR_SECRETS_FIREWORKSAI_APIKEY=
CFG_COMPONENT_SECRETS_OPENAI_APIKEY=
CFG_COMPONENT_SECRETS_STABILITYAI_APIKEY=
CFG_COMPONENT_SECRETS_ANTHROPIC_APIKEY=
CFG_COMPONENT_SECRETS_COHERE_APIKEY=
CFG_COMPONENT_SECRETS_MISTRALAI_APIKEY=
CFG_COMPONENT_SECRETS_GROQ_APIKEY=
CFG_COMPONENT_SECRETS_FIREWORKSAI_APIKEY=

# Numbers Protocol API key.
CFG_CONNECTOR_SECRETS_NUMBERS_XAPIKEY=
CFG_COMPONENT_SECRETS_NUMBERS_XAPIKEY=

# OAuth secrets. When these are filled, the specified component will support
# OAuth integrations.
CFG_CONNECTOR_SECRETS_GOOGLEDRIVE_CLIENTID=
CFG_CONNECTOR_SECRETS_GOOGLEDRIVE_CLIENTSECRET=
CFG_COMPONENT_SECRETS_GOOGLEDRIVE_OAUTHCLIENTID=
CFG_COMPONENT_SECRETS_GOOGLEDRIVE_OAUTHCLIENTSECRET=
CFG_COMPONENT_SECRETS_SLACK_OAUTHCLIENTID=
CFG_COMPONENT_SECRETS_SLACK_OAUTHCLIENTSECRET=
# Dummy values are added here to check OAuth connection creation on integration
# tests.
CFG_COMPONENT_SECRETS_GITHUB_OAUTHCLIENTID=dummy-id
CFG_COMPONENT_SECRETS_GITHUB_OAUTHCLIENTSECRET=dummy-secret
3 changes: 3 additions & 0 deletions .github/workflows/integration-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,10 @@ jobs:
envFile: .env

- name: Launch Instill Core (latest)
# CFG_COMPONENT_SECRETS_GITHUB* variables are injected to test OAuth connection
# creation
run: |
sed -i 's/\(\w\+GITHUB\w\+\)=/\1=foo/' .env.component
COMPOSE_PROFILES=all \
EDITION=local-ce:test \
RAY_LATEST_TAG=latest \
Expand Down
2 changes: 1 addition & 1 deletion cmd/main/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ func main() {
logger.Fatal("failed to create minio client", zap.Error(err))
}
workerUID, _ := uuid.NewV4()
compStore := componentstore.Init(logger, config.Config.Connector.Secrets, nil)
compStore := componentstore.Init(logger, config.Config.Component.Secrets, nil)

service := service.NewService(
repo,
Expand Down
9 changes: 6 additions & 3 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ var Config AppConfig
// AppConfig defines
type AppConfig struct {
Server ServerConfig `koanf:"server"`
Connector ConnectorConfig `koanf:"connector"`
Component ComponentConfig `koanf:"component"`
Database DatabaseConfig `koanf:"database"`
InfluxDB InfluxDBConfig `koanf:"influxdb"`
Temporal TemporalConfig `koanf:"temporal"`
Expand Down Expand Up @@ -83,8 +83,11 @@ type ServerConfig struct {
InstillCoreHost string `koanf:"instillcorehost"`
}

// ConnectorConfig defines the connector configurations
type ConnectorConfig struct {
// ComponentConfig contains the configuration of different components. Global
// secrets may be defined here by component, allowing them to have e.g. a
// default API key when no setup is specified, or to connect with a 3rd party
// vendor via OAuth.
type ComponentConfig struct {
Secrets componentstore.ComponentSecrets
}

Expand Down
2 changes: 1 addition & 1 deletion config/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ server:
instanceid: "pipeline-backend"
datachanbuffersize: 100
instillcorehost: http://localhost:8080
connector:
component:
database:
username: postgres
password: password
Expand Down
35 changes: 32 additions & 3 deletions integration-test/pipeline/rest-integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,11 @@ export function CheckConnections(data) {
[`POST ${path} (dictionary) has a creation time`]: (r) => new Date(r.json().connection.createTime).getTime() > new Date().setTime(0),
});

// Besides an OAuth configuration on the component definition, OAuth
// support requires the client ID and secret to be defined in the config
// (as environment variables). Make sure .env.component contains a client
// secret and ID for GitHub and that it doesn't for Slack.

// Successful creation: OAuth
var oAuthReq = http.request(
"POST",
Expand All @@ -157,6 +162,30 @@ export function CheckConnections(data) {
[`POST ${path} (OAuth) has a creation time`]: (r) => new Date(r.json().connection.createTime).getTime() > new Date().setTime(0),
});

// Check OAuth support.
var unsupportedOAuthReq = http.request(
"POST",
pipelinePublicHost + path,
JSON.stringify({
id: "unsupported-oauth",
integrationId: "slack",
method: "METHOD_OAUTH",
setup: setup,
scopes: ["foo", "bar"],
identity: identity,
oAuthAccessDetails: {
access_token: "one2THREE",
scope: "foo,bar",
token_type: "bearer",
}
}),
data.header
);
check(unsupportedOAuthReq, {
[`POST ${path} response status is 400 when component lacks client ID and secret`]: (r) => r.status === 400,
});


// Check ID format
var invalidID = dbIDPrefix + "This-Is-Invalid";
var invalidIDReq = http.request(
Expand All @@ -180,7 +209,7 @@ export function CheckConnections(data) {
"POST",
pipelinePublicHost + path,
JSON.stringify({
id: invalidID,
id: "invalid-setup",
integrationId: integrationID,
method: "METHOD_OAUTH",
setup: {"token": 234},
Expand All @@ -197,7 +226,7 @@ export function CheckConnections(data) {
"POST",
pipelinePublicHost + path,
JSON.stringify({
id: invalidID,
id: "invalid-method",
integrationId: integrationID,
method: "METHOD_DICTIONARY",
setup: {"token": 234},
Expand Down Expand Up @@ -371,7 +400,7 @@ component:
});

group("Integration API: Update connection", () => {
var path = resourcePath;
var path = resourcePath + "-oauth";
var originalConn = http.request(
"GET",
pipelinePublicHost + path,
Expand Down
32 changes: 32 additions & 0 deletions pkg/component/application/github/v0/component_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -837,3 +837,35 @@ func taskTesting[inType any, outType any](testcases []TaskCase[inType, outType],
})
}
}

func TestComponent_WithOAuthConfig(t *testing.T) {
c := qt.New(t)

bc := base.Component{}

test := func(name string, conf map[string]any, check qt.Checker) {
c.Run(name, func(c *qt.C) {
cmp := Init(bc)
cmp.WithOAuthConfig(conf)
c.Check(cmp.SupportsOAuth(), check)
})
}

newConf := func(clientID, clientSecret string) map[string]any {
conf := map[string]any{}
if clientID != "" {
conf["oauthclientid"] = clientID
}

if clientSecret != "" {
conf["oauthclientsecret"] = clientSecret
}

return conf
}

test("ok - with OAuth details", newConf("foo", "bar"), qt.IsTrue)
test("ok - without OAuth secret", newConf("foo", ""), qt.IsFalse)
test("ok - without OAuth ID", newConf("", "bar"), qt.IsFalse)
test("ok - without OAuth ID", newConf("", ""), qt.IsFalse)
}
6 changes: 6 additions & 0 deletions pkg/component/application/github/v0/main.go
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ @chuang8511 I extracted the OAuth support logic from the individual components as, at least for now, it will be the same code. Have a look at how I added it in the individual components github and slack (both in the component's main and on the store initialiser).

Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ var (

type component struct {
base.Component
base.OAuthConnector
}

type execution struct {
Expand Down Expand Up @@ -139,3 +140,8 @@ func (c *component) ParseEvent(ctx context.Context, req *structpb.Struct, setup
// TODO: parse and validate event
return req, nil
}

// SupportsOAuth checks whether the component is configured to support OAuth.
func (c *component) SupportsOAuth() bool {
return c.OAuthConnector.SupportsOAuth()
}
6 changes: 6 additions & 0 deletions pkg/component/application/slack/v0/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ type SlackClient interface {

type component struct {
base.Component
base.OAuthConnector
}

type execution struct {
Expand Down Expand Up @@ -138,3 +139,8 @@ func (c *component) ParseEvent(ctx context.Context, req *structpb.Struct, setup
// TODO: parse and validate event
return req, nil
}

// SupportsOAuth checks whether the component is configured to support OAuth.
func (c *component) SupportsOAuth() bool {
return c.OAuthConnector.SupportsOAuth()
}
7 changes: 7 additions & 0 deletions pkg/component/base/component.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ type IComponent interface {
Test(sysVars map[string]any, config *structpb.Struct) error

IsSecretField(target string) bool
SupportsOAuth() bool

// Note: These two functions are for the pipeline run-on-event feature,
// which is still experimental and may change at any time.
Expand Down Expand Up @@ -831,6 +832,12 @@ func (c *Component) traverseSecretField(input *structpb.Value, prefix string, se
return secretFields
}

// SupportsOAuth is false by default. To support OAuth, component
// implementations must be composed with `OAuthComponent`.
func (c *Component) SupportsOAuth() bool {
return false
}

func (c *Component) ListInputAcceptFormatsFields() (map[string]map[string][]string, error) {
return c.inputAcceptFormatsFields, nil
}
Expand Down
30 changes: 30 additions & 0 deletions pkg/component/base/oauth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package base

// OAuthConnector contains the OAuth configuration that a comopnent can use to
// support OAuth 2.0 connections. Such components must have an
// `instillOAuthConfig` object in their setup definition.
type OAuthConnector struct {
oAuthClientID string
oAuthClientSecret string
}

const (
cfgOAuthClientID = "oauth-client-id"
cfgOAuthClientSecret = "oauth-client-secret"
)

// WithOAuthConfig loads the OAuth 2.0 connection details into the connector,
// which can be used to determine if the Instill AI deployment supports OAuth
// connections for a given component.
// TODO jvallesm: this is a prerequisite for supporting refresh token when the
// component execution uses an OAuth connection.
func (c *OAuthConnector) WithOAuthConfig(s map[string]any) {
c.oAuthClientID = ReadFromGlobalConfig(cfgOAuthClientID, s)
c.oAuthClientSecret = ReadFromGlobalConfig(cfgOAuthClientSecret, s)

}

// SupportsOAuth checks whether the connector is configured to support OAuth.
func (c *OAuthConnector) SupportsOAuth() bool {
return c.oAuthClientID != "" && c.oAuthClientSecret != ""
}
34 changes: 29 additions & 5 deletions pkg/component/store/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,6 @@ func Init(
compStore.Import(audio.Init(baseComp))
compStore.Import(video.Init(baseComp))

compStore.Import(github.Init(baseComp))
{
// StabilityAI
conn := stabilityai.Init(baseComp)
Expand Down Expand Up @@ -190,7 +189,18 @@ func Init(
compStore.Import(restapi.Init(baseComp))
compStore.Import(collection.Init(baseComp))
compStore.Import(web.Init(baseComp))
compStore.Import(slack.Init(baseComp))
{
// GitHub
conn := github.Init(baseComp)
conn.WithOAuthConfig(secrets[conn.GetDefinitionID()])
compStore.Import(conn)
}
{
// Slack
conn := slack.Init(baseComp)
conn.WithOAuthConfig(secrets[conn.GetDefinitionID()])
compStore.Import(conn)
}
compStore.Import(email.Init(baseComp))
compStore.Import(jira.Init(baseComp))
compStore.Import(ollama.Init(baseComp))
Expand Down Expand Up @@ -298,11 +308,25 @@ func (s *Store) ListDefinitions(sysVars map[string]any, returnTombstone bool) []
return defs
}

// IsSecretField checks whether a property in a component definition is a
// secret field.
func (s *Store) IsSecretField(defUID uuid.UUID, target string) (bool, error) {
if c, ok := s.componentUIDMap[defUID]; ok {
return c.comp.IsSecretField(target), nil
c, ok := s.componentUIDMap[defUID]
if !ok {
return false, ErrComponentDefinitionNotFound
}
return false, ErrComponentDefinitionNotFound

return c.comp.IsSecretField(target), nil
}

// SupportsOAuth checks whether a component supports OAuth connections.
func (s *Store) SupportsOAuth(defUID uuid.UUID) (bool, error) {
c, ok := s.componentUIDMap[defUID]
if !ok {
return false, ErrComponentDefinitionNotFound
}

return c.comp.SupportsOAuth(), nil
}

// ErrComponentDefinitionNotFound is returned when trying to access an
Expand Down
Loading
Loading