diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 543d5eac504cc65d04cdaba7d171851fb18e1194..3f10ae8176730529b8344e16ef45530b1d7c246c 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -137,6 +137,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :user_default_external, :user_oauth_applications, :version_check_enabled, + :terminal_max_session_time, disabled_oauth_sign_in_sources: [], import_sources: [], diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 2df8b071e13e0165f8db82324d4a0c4b9b2cc8ca..9a4557524c4b0be542b35b28c208ee0627d66c32 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -111,6 +111,10 @@ class ApplicationSetting < ActiveRecord::Base presence: true, numericality: { only_integer: true, greater_than: :housekeeping_full_repack_period } + validates :terminal_max_session_time, + presence: true, + numericality: { only_integer: true, greater_than_or_equal_to: 0 } + validates_each :restricted_visibility_levels do |record, attr, value| unless value.nil? value.each do |level| @@ -204,7 +208,8 @@ class ApplicationSetting < ActiveRecord::Base signin_enabled: Settings.gitlab['signin_enabled'], signup_enabled: Settings.gitlab['signup_enabled'], two_factor_grace_period: 48, - user_default_external: false + user_default_external: false, + terminal_max_session_time: 0 } end diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb index fa3cedc4354bbdfcaab1e9f4a43683b97079d6e2..f2f019c43bb46f5b6a4d112098683351901fc3e8 100644 --- a/app/models/project_services/kubernetes_service.rb +++ b/app/models/project_services/kubernetes_service.rb @@ -1,4 +1,5 @@ class KubernetesService < DeploymentService + include Gitlab::CurrentSettings include Gitlab::Kubernetes include ReactiveCaching @@ -110,7 +111,7 @@ class KubernetesService < DeploymentService pods = data.fetch(:pods, nil) filter_pods(pods, app: environment.slug). flat_map { |pod| terminals_for_pod(api_url, namespace, pod) }. - map { |terminal| add_terminal_auth(terminal, token, ca_pem) } + each { |terminal| add_terminal_auth(terminal, terminal_auth) } end end @@ -170,4 +171,12 @@ class KubernetesService < DeploymentService url.to_s end + + def terminal_auth + { + token: token, + ca_pem: ca_pem, + max_session_time: current_application_settings.terminal_max_session_time + } + end end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index e7701d75a6eec843176d11e9223a4d8ffd13c070..2c64de1d5304019de03e44a3e36428810a6d510c 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -509,5 +509,15 @@ .help-block Number of Git pushes after which 'git gc' is run. + %fieldset + %legend Web terminal + .form-group + = f.label :terminal_max_session_time, 'Max session time', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :terminal_max_session_time, class: 'form-control' + .help-block + Maximum time for web terminal websocket connection (in seconds). + Set to 0 for unlimited time. + .form-actions = f.submit 'Save', class: 'btn btn-save' diff --git a/changelogs/unreleased/terminal-max-session-time.yml b/changelogs/unreleased/terminal-max-session-time.yml new file mode 100644 index 0000000000000000000000000000000000000000..db1e66770d1434dea0bed3ee04fb468e59aa0019 --- /dev/null +++ b/changelogs/unreleased/terminal-max-session-time.yml @@ -0,0 +1,4 @@ +--- +title: Introduce maximum session time for terminal websocket connection +merge_request: 8413 +author: diff --git a/db/migrate/20170126174819_add_terminal_max_session_time_to_application_settings.rb b/db/migrate/20170126174819_add_terminal_max_session_time_to_application_settings.rb new file mode 100644 index 0000000000000000000000000000000000000000..334f53f9145f3b3147463098431dd30586cafb9c --- /dev/null +++ b/db/migrate/20170126174819_add_terminal_max_session_time_to_application_settings.rb @@ -0,0 +1,33 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddTerminalMaxSessionTimeToApplicationSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + # When a migration requires downtime you **must** uncomment the following + # constant and define a short and easy to understand explanation as to why the + # migration requires downtime. + # DOWNTIME_REASON = '' + + # When using the methods "add_concurrent_index" or "add_column_with_default" + # you must disable the use of transactions as these methods can not run in an + # existing transaction. When using "add_concurrent_index" make sure that this + # method is the _only_ method called in the migration, any other changes + # should go in a separate migration. This ensures that upon failure _only_ the + # index creation fails and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + disable_ddl_transaction! + + def up + add_column_with_default :application_settings, :terminal_max_session_time, :integer, default: 0, allow_null: false + end + + def down + remove_column :application_settings, :terminal_max_session_time + end +end diff --git a/db/schema.rb b/db/schema.rb index 92b36218a15adfc7cf50e5ea4a0f8c571a09fa7c..a9f4e865d60b23327c286efc9dd0480834cbd1b8 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -109,6 +109,7 @@ ActiveRecord::Schema.define(version: 20170204181513) do t.boolean "html_emails_enabled", default: true t.string "plantuml_url" t.boolean "plantuml_enabled" + t.integer "terminal_max_session_time", default: 0, null: false end create_table "audit_events", force: :cascade do |t| diff --git a/doc/administration/integration/terminal.md b/doc/administration/integration/terminal.md index 3fbb13704aaec66cf111b0a88d0677f1f798b0ab..1144446453756e198ce61efd4bf6d3eb0106ed10 100644 --- a/doc/administration/integration/terminal.md +++ b/doc/administration/integration/terminal.md @@ -71,5 +71,15 @@ When these headers are not passed through, Workhorse will return a `400 Bad Request` response to users attempting to use a web terminal. In turn, they will receive a `Connection failed` message. +## Limiting WebSocket connection time + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8413) +in GitLab 8.17. + +Terminal sessions use long-lived connections; by default, these may last +forever. You can configure a maximum session time in the Admin area of your +GitLab instance if you find this undesirable from a scalability or security +point of view. + [ce-7690]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7690 [kubservice]: ../../user/project/integrations/kubernetes.md) diff --git a/doc/api/settings.md b/doc/api/settings.md index f86c7cc2f941f016f385efd22f1cdafe4a7ebb59..ca6b934787733d1d8b8739fe0f40ce8de1e16c70 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -46,7 +46,8 @@ Example response: "koding_enabled": false, "koding_url": null, "plantuml_enabled": false, - "plantuml_url": null + "plantuml_url": null, + "terminal_max_session_time": 0 } ``` @@ -84,6 +85,7 @@ PUT /application/settings | `disabled_oauth_sign_in_sources` | Array of strings | no | Disabled OAuth sign-in sources | | `plantuml_enabled` | boolean | no | Enable PlantUML integration. Default is `false`. | | `plantuml_url` | string | yes (if `plantuml_enabled` is `true`) | The PlantUML instance URL for integration. | +| `terminal_max_session_time` | integer | no | Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time. | ```bash curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/application/settings?signup_enabled=false&default_project_visibility=1 @@ -118,6 +120,7 @@ Example response: "koding_enabled": false, "koding_url": null, "plantuml_enabled": false, - "plantuml_url": null + "plantuml_url": null, + "terminal_max_session_time": 0 } ``` diff --git a/lib/api/entities.rb b/lib/api/entities.rb index a07b2a9ca0fa5fa01768f3f01b1be6fabbafc71d..b1ead48caf7e6212f4d681a4517200ea4147bb7a 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -575,6 +575,7 @@ module API expose :koding_url expose :plantuml_enabled expose :plantuml_url + expose :terminal_max_session_time end class Release < Grape::Entity diff --git a/lib/api/settings.rb b/lib/api/settings.rb index c5eff16a5de222f8e0c1de18c195ec967b1dc9ad..a1d1c1432d3c44d92c7e9e1d8d3072e4acb0e9f1 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -107,6 +107,7 @@ module API requires :housekeeping_full_repack_period, type: Integer, desc: "Number of Git pushes after which a full 'git repack' is run." requires :housekeeping_gc_period, type: Integer, desc: "Number of Git pushes after which 'git gc' is run." end + optional :terminal_max_session_time, type: Integer, desc: 'Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time.' at_least_one_of :default_branch_protection, :default_project_visibility, :default_snippet_visibility, :default_group_visibility, :restricted_visibility_levels, :import_sources, :enabled_git_access_protocol, :gravatar_enabled, :default_projects_limit, @@ -120,7 +121,7 @@ module API :akismet_enabled, :admin_notification_email, :sentry_enabled, :repository_storage, :repository_checks_enabled, :koding_enabled, :plantuml_enabled, :version_check_enabled, :email_author_in_body, :html_emails_enabled, - :housekeeping_enabled + :housekeeping_enabled, :terminal_max_session_time end put "application/settings" do if current_settings.update_attributes(declared_params(include_missing: false)) diff --git a/lib/gitlab/kubernetes.rb b/lib/gitlab/kubernetes.rb index 288771c1c124d0bdd67b6ed161c1877267597c5f..3a7af363548b87137982ce721450ca79493605ba 100644 --- a/lib/gitlab/kubernetes.rb +++ b/lib/gitlab/kubernetes.rb @@ -43,10 +43,10 @@ module Gitlab end end - def add_terminal_auth(terminal, token, ca_pem = nil) + def add_terminal_auth(terminal, token:, max_session_time:, ca_pem: nil) terminal[:headers]['Authorization'] << "Bearer #{token}" + terminal[:max_session_time] = max_session_time terminal[:ca_pem] = ca_pem if ca_pem.present? - terminal end def container_exec_url(api_url, namespace, pod_name, container_name) diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb index a3b502ffd6a752e83ebbbbc49a8a9c1b6eec48d1..c8872df8a935e54b424c1fcb0a0eb283a45ef4d7 100644 --- a/lib/gitlab/workhorse.rb +++ b/lib/gitlab/workhorse.rb @@ -107,7 +107,8 @@ module Gitlab 'Terminal' => { 'Subprotocols' => terminal[:subprotocols], 'Url' => terminal[:url], - 'Header' => terminal[:headers] + 'Header' => terminal[:headers], + 'MaxSessionTime' => terminal[:max_session_time], } } details['Terminal']['CAPem'] = terminal[:ca_pem] if terminal.has_key?(:ca_pem) diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index 7dd4d76d1a39e27a7b32ad8f3e72f99051d21c6f..a32c6131030e619839d3e95436efcf055ea2acf7 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -42,7 +42,8 @@ describe Gitlab::Workhorse, lib: true do out = { subprotocols: ['foo'], url: 'wss://example.com/terminal.ws', - headers: { 'Authorization' => ['Token x'] } + headers: { 'Authorization' => ['Token x'] }, + max_session_time: 600 } out[:ca_pem] = ca_pem if ca_pem out @@ -53,7 +54,8 @@ describe Gitlab::Workhorse, lib: true do 'Terminal' => { 'Subprotocols' => ['foo'], 'Url' => 'wss://example.com/terminal.ws', - 'Header' => { 'Authorization' => ['Token x'] } + 'Header' => { 'Authorization' => ['Token x'] }, + 'MaxSessionTime' => 600 } } out['Terminal']['CAPem'] = ca_pem if ca_pem diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb index 4f3cd14e941322efb4094e11c09ba3c1e8d8b274..9052479d35eb48916b71783b9dccd20445d05069 100644 --- a/spec/models/project_services/kubernetes_service_spec.rb +++ b/spec/models/project_services/kubernetes_service_spec.rb @@ -181,11 +181,23 @@ describe KubernetesService, models: true, caching: true do let(:pod) { kube_pod(app: environment.slug) } let(:terminals) { kube_terminals(service, pod) } - it 'returns terminals' do - stub_reactive_cache(service, pods: [ pod, pod, kube_pod(app: "should-be-filtered-out") ]) + before do + stub_reactive_cache( + service, + pods: [ pod, pod, kube_pod(app: "should-be-filtered-out") ] + ) + end + it 'returns terminals' do is_expected.to eq(terminals + terminals) end + + it 'uses max session time from settings' do + stub_application_setting(terminal_max_session_time: 600) + + times = subject.map { |terminal| terminal[:max_session_time] } + expect(times).to eq [600, 600, 600, 600] + end end end diff --git a/spec/support/kubernetes_helpers.rb b/spec/support/kubernetes_helpers.rb index 6c4c246a68b3357be3f0c2c6aa74e1bab4d45d48..444612cf8713091a75762c2a0e83fbb980478aa3 100644 --- a/spec/support/kubernetes_helpers.rb +++ b/spec/support/kubernetes_helpers.rb @@ -43,7 +43,8 @@ module KubernetesHelpers url: container_exec_url(service.api_url, service.namespace, pod_name, container['name']), subprotocols: ['channel.k8s.io'], headers: { 'Authorization' => ["Bearer #{service.token}"] }, - created_at: DateTime.parse(pod['metadata']['creationTimestamp']) + created_at: DateTime.parse(pod['metadata']['creationTimestamp']), + max_session_time: 0 } terminal[:ca_pem] = service.ca_pem if service.ca_pem.present? terminal