diff --git a/.circleci/config.yml b/.circleci/config.yml index b23efce7..ed0d5eb0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -29,7 +29,6 @@ jobs: - POSTGRES_USER=postgres - POSTGRES_PWD=temporal - POSTGRES_SEEDS=postgres - - DYNAMIC_CONFIG_FILE_PATH=config/dynamicconfig/development.yaml environment: - TEMPORAL_HOST=temporal diff --git a/README.md b/README.md index 838fda7a..7f5ff27d 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ Temporal.configure do |config| config.port = 7233 config.namespace = 'ruby-samples' config.task_queue = 'hello-world' + config.credentials = :this_channel_is_insecure end begin @@ -114,6 +115,57 @@ curl -O https://raw.githubusercontent.com/temporalio/docker-compose/main/docker- docker-compose up ``` +## Using Credentials + +### SSL + +In many production deployments you will end up connecting to your Temporal Services via SSL. In this +case you must read the public certificate of the CA that issued your Temporal server's SSL certificate and create +an instance of [gRPC Channel Credentials](https://grpc.io/docs/guides/auth/#with-server-authentication-ssltls-1). + +Configure your Temporal connection: + +```ruby +Temporal.configure do |config| + config.host = 'localhost' + config.port = 7233 + config.namespace = 'ruby-samples' + config.task_queue = 'hello-world' + config.credentials = GRPC::Core::ChannelCredentials.new(root_cert, client_key, client_chain) +end +``` + +### OAuth2 Token + +Use gRPC Call Credentials to add OAuth2 token to gRPC calls: + +```ruby +Temporal.configure do |config| + config.host = 'localhost' + config.port = 7233 + config.namespace = 'ruby-samples' + config.task_queue = 'hello-world' + config.credentials = GRPC::Core::CallCredentials.new(updater_proc) +end +``` +`updater_proc` should be a method that returns `proc`. See an example of `updater_proc` in [googleauth](https://www.rubydoc.info/gems/googleauth/0.1.0/Signet/OAuth2/Client) library. + +### Combining Credentials + +To configure both SSL and OAuth2 token cedentials use `compose` method: + +```ruby +Temporal.configure do |config| + config.host = 'localhost' + config.port = 7233 + config.namespace = 'ruby-samples' + config.task_queue = 'hello-world' + config.credentials = GRPC::Core::ChannelCredentials.new(root_cert, client_key, client_chain).compose( + GRPC::Core::CallCredentials.new(token.updater_proc) + ) +end +``` + ## Workflows A workflow is defined using pure Ruby code, however it should contain only a high-level diff --git a/lib/temporal/configuration.rb b/lib/temporal/configuration.rb index 8d36615c..d2edaead 100644 --- a/lib/temporal/configuration.rb +++ b/lib/temporal/configuration.rb @@ -7,12 +7,12 @@ module Temporal class Configuration - Connection = Struct.new(:type, :host, :port, keyword_init: true) + Connection = Struct.new(:type, :host, :port, :credentials, keyword_init: true) Execution = Struct.new(:namespace, :task_queue, :timeouts, :headers, keyword_init: true) attr_reader :timeouts, :error_handlers attr_writer :converter - attr_accessor :connection_type, :host, :port, :logger, :metrics_adapter, :namespace, :task_queue, :headers + attr_accessor :connection_type, :host, :port, :credentials, :logger, :metrics_adapter, :namespace, :task_queue, :headers # See https://docs.temporal.io/blog/activity-timeouts/ for general docs. # We want an infinite execution timeout for cron schedules and other perpetual workflows. @@ -53,6 +53,7 @@ def initialize @headers = DEFAULT_HEADERS @converter = DEFAULT_CONVERTER @error_handlers = [] + @credentials = :this_channel_is_insecure end def on_error(&block) @@ -79,7 +80,8 @@ def for_connection Connection.new( type: connection_type, host: host, - port: port + port: port, + credentials: credentials ).freeze end diff --git a/lib/temporal/connection.rb b/lib/temporal/connection.rb index b499ca73..791534bf 100644 --- a/lib/temporal/connection.rb +++ b/lib/temporal/connection.rb @@ -10,12 +10,13 @@ def self.generate(configuration) connection_class = CLIENT_TYPES_MAP[configuration.type] host = configuration.host port = configuration.port + credentials = configuration.credentials hostname = `hostname` thread_id = Thread.current.object_id identity = "#{thread_id}@#{hostname}" - connection_class.new(host, port, identity) + connection_class.new(host, port, identity, credentials) end end end diff --git a/lib/temporal/connection/grpc.rb b/lib/temporal/connection/grpc.rb index dd306b97..9b0d9699 100644 --- a/lib/temporal/connection/grpc.rb +++ b/lib/temporal/connection/grpc.rb @@ -31,9 +31,10 @@ class GRPC max_page_size: 100 }.freeze - def initialize(host, port, identity, options = {}) + def initialize(host, port, identity, credentials, options = {}) @url = "#{host}:#{port}" @identity = identity + @credentials = credentials @poll = true @poll_mutex = Mutex.new @poll_request = nil @@ -536,12 +537,12 @@ def cancel_polling_request private - attr_reader :url, :identity, :options, :poll_mutex, :poll_request + attr_reader :url, :identity, :credentials, :options, :poll_mutex, :poll_request def client @client ||= Temporal::Api::WorkflowService::V1::WorkflowService::Stub.new( url, - :this_channel_is_insecure, + credentials, timeout: 60 ) end diff --git a/spec/unit/lib/temporal/connection_spec.rb b/spec/unit/lib/temporal/connection_spec.rb new file mode 100644 index 00000000..dff88b0a --- /dev/null +++ b/spec/unit/lib/temporal/connection_spec.rb @@ -0,0 +1,58 @@ +describe Temporal::Connection do + subject { described_class.generate(config.for_connection) } + + let(:connection_type) { :grpc } + let(:credentials) { nil } + let(:config) do + config = Temporal::Configuration.new + config.connection_type = connection_type + config.credentials = credentials if credentials + config + end + + context 'insecure' do + let(:credentials) { :this_channel_is_insecure } + + it 'generates a grpc connection' do + expect(subject).to be_kind_of(Temporal::Connection::GRPC) + expect(subject.send(:identity)).not_to be_nil + expect(subject.send(:credentials)).to eq(:this_channel_is_insecure) + end + end + + context 'ssl' do + let(:credentials) { GRPC::Core::ChannelCredentials.new } + + it 'generates a grpc connection' do + expect(subject).to be_kind_of(Temporal::Connection::GRPC) + expect(subject.send(:identity)).not_to be_nil + expect(subject.send(:credentials)).to be_kind_of(GRPC::Core::ChannelCredentials) + end + end + + context 'oauth2' do + let(:credentials) { GRPC::Core::CallCredentials.new(proc { { authorization: 'token' } }) } + + it 'generates a grpc connection' do + expect(subject).to be_kind_of(Temporal::Connection::GRPC) + expect(subject.send(:identity)).not_to be_nil + expect(subject.send(:credentials)).to be_kind_of(GRPC::Core::CallCredentials) + end + end + + context 'ssl + oauth2' do + let(:credentials) do + GRPC::Core::ChannelCredentials.new.compose( + GRPC::Core::CallCredentials.new( + proc { { authorization: 'token' } } + ) + ) + end + + it 'generates a grpc connection' do + expect(subject).to be_kind_of(Temporal::Connection::GRPC) + expect(subject.send(:identity)).not_to be_nil + expect(subject.send(:credentials)).to be_kind_of(GRPC::Core::ChannelCredentials) + end + end +end diff --git a/spec/unit/lib/temporal/grpc_spec.rb b/spec/unit/lib/temporal/grpc_spec.rb index d828d537..ef617341 100644 --- a/spec/unit/lib/temporal/grpc_spec.rb +++ b/spec/unit/lib/temporal/grpc_spec.rb @@ -10,7 +10,7 @@ let(:run_id) { SecureRandom.uuid } let(:now) { Time.now} - subject { Temporal::Connection::GRPC.new(nil, nil, identity) } + subject { Temporal::Connection::GRPC.new(nil, nil, identity, :this_channel_is_insecure) } class TestDeserializer extend Temporal::Concerns::Payloads