diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-03-11 06:08:14 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-03-11 06:08:14 +0300 |
commit | b131b6f6804bbcd22a22ce4ffe6566a384843f26 (patch) | |
tree | 12556081b23ccc647a73faf3af2f9f38f84bbb29 /app | |
parent | ca443618b0decc6b2754bf560a9c7a319d3e9873 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
25 files changed, 439 insertions, 25 deletions
diff --git a/app/assets/javascripts/authentication/webauthn/util.js b/app/assets/javascripts/authentication/webauthn/util.js index eeda2bfaeaf..2a0740cf488 100644 --- a/app/assets/javascripts/authentication/webauthn/util.js +++ b/app/assets/javascripts/authentication/webauthn/util.js @@ -46,6 +46,17 @@ export const bufferToBase64 = (input) => { }; /** + * Return a URL-safe base64 string. + * + * RFC: https://datatracker.ietf.org/doc/html/rfc4648#section-5 + * @param {String} base64Str + * @returns {String} + */ +export const base64ToBase64Url = (base64Str) => { + return base64Str.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); +}; + +/** * Returns a copy of the given object with the id property converted to buffer * * @param {Object} param diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/app.vue b/app/assets/javascripts/jira_connect/subscriptions/components/app.vue index 905e242e977..afdb414e82c 100644 --- a/app/assets/javascripts/jira_connect/subscriptions/components/app.vue +++ b/app/assets/javascripts/jira_connect/subscriptions/components/app.vue @@ -3,6 +3,7 @@ import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui'; import { isEmpty } from 'lodash'; import { mapState, mapMutations } from 'vuex'; import { retrieveAlert } from '~/jira_connect/subscriptions/utils'; +import { I18N_DEFAULT_SIGN_IN_ERROR_MESSAGE } from '../constants'; import { SET_ALERT } from '../store/mutation_types'; import SignInPage from '../pages/sign_in.vue'; import SubscriptionsPage from '../pages/subscriptions.vue'; @@ -28,6 +29,11 @@ export default { default: [], }, }, + data() { + return { + user: null, + }; + }, computed: { ...mapState(['alert']), shouldShowAlert() { @@ -37,7 +43,7 @@ export default { return !isEmpty(this.subscriptions); }, userSignedIn() { - return Boolean(!this.usersPath); + return Boolean(!this.usersPath || this.user); }, }, created() { @@ -51,6 +57,15 @@ export default { const { linkUrl, title, message, variant } = retrieveAlert() || {}; this.setAlert({ linkUrl, title, message, variant }); }, + onSignInOauth(user) { + this.user = user; + }, + onSignInError() { + this.setAlert({ + message: I18N_DEFAULT_SIGN_IN_ERROR_MESSAGE, + variant: 'danger', + }); + }, }, }; </script> @@ -78,11 +93,16 @@ export default { </template> </gl-alert> - <user-link :user-signed-in="userSignedIn" :has-subscriptions="hasSubscriptions" /> + <user-link :user-signed-in="userSignedIn" :has-subscriptions="hasSubscriptions" :user="user" /> <h2 class="gl-text-center gl-mb-7">{{ s__('JiraService|GitLab for Jira Configuration') }}</h2> <div class="gl-layout-w-limited gl-mx-auto gl-px-5 gl-mb-7"> - <sign-in-page v-if="!userSignedIn" :has-subscriptions="hasSubscriptions" /> + <sign-in-page + v-if="!userSignedIn" + :has-subscriptions="hasSubscriptions" + @sign-in-oauth="onSignInOauth" + @error="onSignInError" + /> <subscriptions-page v-else :has-subscriptions="hasSubscriptions" /> </div> </div> diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/sign_in_button.vue b/app/assets/javascripts/jira_connect/subscriptions/components/sign_in_legacy_button.vue index 627abcdd4a0..ec718d5b3ca 100644 --- a/app/assets/javascripts/jira_connect/subscriptions/components/sign_in_button.vue +++ b/app/assets/javascripts/jira_connect/subscriptions/components/sign_in_legacy_button.vue @@ -1,7 +1,7 @@ <script> import { GlButton } from '@gitlab/ui'; import { getGitlabSignInURL } from '~/jira_connect/subscriptions/utils'; -import { s__ } from '~/locale'; +import { I18N_DEFAULT_SIGN_IN_BUTTON_TEXT } from '~/jira_connect/subscriptions/constants'; export default { components: { @@ -27,7 +27,7 @@ export default { }, }, i18n: { - defaultButtonText: s__('Integrations|Sign in to GitLab'), + defaultButtonText: I18N_DEFAULT_SIGN_IN_BUTTON_TEXT, }, }; </script> diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/sign_in_oauth_button.vue b/app/assets/javascripts/jira_connect/subscriptions/components/sign_in_oauth_button.vue new file mode 100644 index 00000000000..d7ec909cb28 --- /dev/null +++ b/app/assets/javascripts/jira_connect/subscriptions/components/sign_in_oauth_button.vue @@ -0,0 +1,124 @@ +<script> +import { GlButton } from '@gitlab/ui'; +import axios from '~/lib/utils/axios_utils'; +import { + I18N_DEFAULT_SIGN_IN_BUTTON_TEXT, + OAUTH_WINDOW_OPTIONS, + PKCE_CODE_CHALLENGE_DIGEST_ALGORITHM, +} from '~/jira_connect/subscriptions/constants'; +import { setUrlParams } from '~/lib/utils/url_utility'; +import AccessorUtilities from '~/lib/utils/accessor'; + +import { createCodeVerifier, createCodeChallenge } from '../pkce'; + +export default { + components: { + GlButton, + }, + inject: ['oauthMetadata'], + data() { + return { + token: null, + loading: false, + codeVerifier: null, + canUseCrypto: AccessorUtilities.canUseCrypto(), + }; + }, + mounted() { + window.addEventListener('message', this.handleWindowMessage); + }, + beforeDestroy() { + window.removeEventListener('message', this.handleWindowMessage); + }, + methods: { + async startOAuthFlow() { + this.loading = true; + + // Generate state necessary for PKCE OAuth flow + this.codeVerifier = createCodeVerifier(); + const codeChallenge = await createCodeChallenge(this.codeVerifier); + + // Build the initial OAuth authorization URL + const { oauth_authorize_url: oauthAuthorizeURL } = this.oauthMetadata; + const oauthAuthorizeURLWithChallenge = setUrlParams( + { + code_challenge: codeChallenge, + code_challenge_method: PKCE_CODE_CHALLENGE_DIGEST_ALGORITHM.short, + }, + oauthAuthorizeURL, + ); + + window.open( + oauthAuthorizeURLWithChallenge, + this.$options.i18n.defaultButtonText, + OAUTH_WINDOW_OPTIONS, + ); + }, + async handleWindowMessage(event) { + if (window.origin !== event.origin) { + this.loading = false; + this.handleError(); + return; + } + + // Verify that OAuth state isn't altered. + const state = event.data?.state; + if (state !== this.oauthMetadata.state) { + this.loading = false; + this.handleError(); + return; + } + + // Request access token and load the authenticated user. + const code = event.data?.code; + try { + const accessToken = await this.getOAuthToken(code); + await this.loadUser(accessToken); + } catch (e) { + this.handleError(); + } finally { + this.loading = false; + } + }, + handleError() { + this.$emit('error'); + }, + async getOAuthToken(code) { + const { + oauth_token_payload: oauthTokenPayload, + oauth_token_url: oauthTokenURL, + } = this.oauthMetadata; + const { data } = await axios.post(oauthTokenURL, { + ...oauthTokenPayload, + code, + code_verifier: this.codeVerifier, + }); + + return data.access_token; + }, + async loadUser(accessToken) { + const { data } = await axios.get('/api/v4/user', { + headers: { Authorization: `Bearer ${accessToken}` }, + }); + + this.$emit('sign-in', data); + }, + }, + i18n: { + defaultButtonText: I18N_DEFAULT_SIGN_IN_BUTTON_TEXT, + }, +}; +</script> +<template> + <gl-button + category="primary" + variant="info" + :loading="loading" + :disabled="!canUseCrypto" + @click="startOAuthFlow" + > + <slot> + {{ $options.i18n.defaultButtonText }} + </slot> + </gl-button> +</template> diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/user_link.vue b/app/assets/javascripts/jira_connect/subscriptions/components/user_link.vue index fad3d2616d8..5e2c83aff65 100644 --- a/app/assets/javascripts/jira_connect/subscriptions/components/user_link.vue +++ b/app/assets/javascripts/jira_connect/subscriptions/components/user_link.vue @@ -25,6 +25,11 @@ export default { type: Boolean, required: true, }, + user: { + type: Object, + required: false, + default: null, + }, }, data() { return { @@ -32,8 +37,19 @@ export default { }; }, computed: { + gitlabUserName() { + return gon.current_username ?? this.user?.username; + }, gitlabUserHandle() { - return `@${gon.current_username}`; + return this.gitlabUserName ? `@${this.gitlabUserName}` : undefined; + }, + gitlabUserLink() { + return this.gitlabUserPath ?? `${gon.relative_root_url}/${this.gitlabUserName}`; + }, + signedInText() { + return this.gitlabUserHandle + ? this.$options.i18n.signedInAsUserText + : this.$options.i18n.signedInText; }, }, async created() { @@ -42,14 +58,15 @@ export default { i18n: { signInText: __('Sign in to GitLab'), signedInAsUserText: __('Signed in to GitLab as %{user_link}'), + signedInText: __('Signed in to GitLab'), }, }; </script> <template> <div class="jira-connect-user gl-font-base"> - <gl-sprintf v-if="userSignedIn" :message="$options.i18n.signedInAsUserText"> + <gl-sprintf v-if="userSignedIn" :message="signedInText"> <template #user_link> - <gl-link data-testid="gitlab-user-link" :href="gitlabUserPath" target="_blank"> + <gl-link data-testid="gitlab-user-link" :href="gitlabUserLink" target="_blank"> {{ gitlabUserHandle }} </gl-link> </template> diff --git a/app/assets/javascripts/jira_connect/subscriptions/constants.js b/app/assets/javascripts/jira_connect/subscriptions/constants.js index 2a65b7bc1fa..d30ebdbb487 100644 --- a/app/assets/javascripts/jira_connect/subscriptions/constants.js +++ b/app/assets/javascripts/jira_connect/subscriptions/constants.js @@ -1,5 +1,26 @@ +import { s__ } from '~/locale'; + export const DEFAULT_GROUPS_PER_PAGE = 10; export const ALERT_LOCALSTORAGE_KEY = 'gitlab_alert'; export const MINIMUM_SEARCH_TERM_LENGTH = 3; export const ADD_NAMESPACE_MODAL_ID = 'add-namespace-modal'; + +export const I18N_DEFAULT_SIGN_IN_BUTTON_TEXT = s__('Integrations|Sign in to GitLab'); +export const I18N_DEFAULT_SIGN_IN_ERROR_MESSAGE = s__('Integrations|Failed to sign in to GitLab.'); + +const OAUTH_WINDOW_SIZE = 800; +export const OAUTH_WINDOW_OPTIONS = [ + 'resizable=yes', + 'scrollbars=yes', + 'status=yes', + `width=${OAUTH_WINDOW_SIZE}`, + `height=${OAUTH_WINDOW_SIZE}`, + `left=${window.screen.width / 2 - OAUTH_WINDOW_SIZE / 2}`, + `top=${window.screen.height / 2 - OAUTH_WINDOW_SIZE / 2}`, +].join(','); + +export const PKCE_CODE_CHALLENGE_DIGEST_ALGORITHM = { + long: 'SHA-256', + short: 'S256', +}; diff --git a/app/assets/javascripts/jira_connect/subscriptions/index.js b/app/assets/javascripts/jira_connect/subscriptions/index.js index cd1fc1d4455..320f0f8aa6c 100644 --- a/app/assets/javascripts/jira_connect/subscriptions/index.js +++ b/app/assets/javascripts/jira_connect/subscriptions/index.js @@ -21,7 +21,14 @@ export function initJiraConnect() { Vue.use(Translate); Vue.use(GlFeatureFlagsPlugin); - const { groupsPath, subscriptions, subscriptionsPath, usersPath, gitlabUserPath } = el.dataset; + const { + groupsPath, + subscriptions, + subscriptionsPath, + usersPath, + gitlabUserPath, + oauthMetadata, + } = el.dataset; sizeToParent(); return new Vue({ @@ -33,6 +40,7 @@ export function initJiraConnect() { subscriptionsPath, usersPath, gitlabUserPath, + oauthMetadata: oauthMetadata ? JSON.parse(oauthMetadata) : null, }, render(createElement) { return createElement(JiraConnectApp); diff --git a/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in.vue b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in.vue index 2bce5afc72b..a24ee33b723 100644 --- a/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in.vue +++ b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in.vue @@ -1,14 +1,17 @@ <script> import { s__ } from '~/locale'; + +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import SubscriptionsList from '../components/subscriptions_list.vue'; -import SignInButton from '../components/sign_in_button.vue'; export default { name: 'SignInPage', components: { SubscriptionsList, - SignInButton, + SignInLegacyButton: () => import('../components/sign_in_legacy_button.vue'), + SignInOauthButton: () => import('../components/sign_in_oauth_button.vue'), }, + mixins: [glFeatureFlagMixin()], inject: ['usersPath'], props: { hasSubscriptions: { @@ -16,25 +19,47 @@ export default { required: true, }, }, + computed: { + useSignInOauthButton() { + return this.glFeatures.jiraConnectOauth; + }, + }, i18n: { - signinButtonTextWithSubscriptions: s__('Integrations|Sign in to add namespaces'), + signInButtonTextWithSubscriptions: s__('Integrations|Sign in to add namespaces'), signInText: s__('JiraService|Sign in to GitLab.com to get started.'), }, + methods: { + onSignInError() { + this.$emit('error'); + }, + }, }; </script> <template> <div v-if="hasSubscriptions"> <div class="gl-display-flex gl-justify-content-end"> - <sign-in-button :users-path="usersPath"> - {{ $options.i18n.signinButtonTextWithSubscriptions }} - </sign-in-button> + <sign-in-oauth-button + v-if="useSignInOauthButton" + @sign-in="$emit('sign-in-oauth', $event)" + @error="onSignInError" + > + {{ $options.i18n.signInButtonTextWithSubscriptions }} + </sign-in-oauth-button> + <sign-in-legacy-button v-else :users-path="usersPath"> + {{ $options.i18n.signInButtonTextWithSubscriptions }} + </sign-in-legacy-button> </div> <subscriptions-list /> </div> <div v-else class="gl-text-center"> <p class="gl-mb-7">{{ $options.i18n.signInText }}</p> - <sign-in-button class="gl-mb-7" :users-path="usersPath" /> + <sign-in-oauth-button + v-if="useSignInOauthButton" + @sign-in="$emit('sign-in-oauth', $event)" + @error="onSignInError" + /> + <sign-in-legacy-button v-else class="gl-mb-7" :users-path="usersPath" /> </div> </template> diff --git a/app/assets/javascripts/jira_connect/subscriptions/pkce.js b/app/assets/javascripts/jira_connect/subscriptions/pkce.js new file mode 100644 index 00000000000..18ea5cae860 --- /dev/null +++ b/app/assets/javascripts/jira_connect/subscriptions/pkce.js @@ -0,0 +1,60 @@ +import { bufferToBase64, base64ToBase64Url } from '~/authentication/webauthn/util'; +import { PKCE_CODE_CHALLENGE_DIGEST_ALGORITHM } from './constants'; + +// PKCE codeverifier should have a maximum length of 128 characters. +// Using 96 bytes generates a string of 128 characters. +// RFC: https://datatracker.ietf.org/doc/html/rfc7636#section-4.1 +export const CODE_VERIFIER_BYTES = 96; + +/** + * Generate a cryptographically random string. + * @param {Number} lengthBytes + * @returns {String} a random string + */ +function getRandomString(lengthBytes) { + // generate random values and load them into byteArray. + const byteArray = new Uint8Array(lengthBytes); + window.crypto.getRandomValues(byteArray); + + // Convert array to string + const randomString = bufferToBase64(byteArray); + return randomString; +} + +/** + * Creates a code verifier to be used for OAuth PKCE authentication. + * The code verifier has 128 characters. + * + * RFC: https://datatracker.ietf.org/doc/html/rfc7636#section-4.1 + * @returns {String} code verifier + */ +export function createCodeVerifier() { + const verifier = getRandomString(CODE_VERIFIER_BYTES); + return base64ToBase64Url(verifier); +} + +/** + * Creates a code challenge for OAuth PKCE authentication. + * The code challenge is derived from the given [codeVerifier]. + * [codeVerifier] is tranformed in the following way (as per the RFC): + * code_challenge = BASE64URL-ENCODE(SHA256(ASCII(codeVerifier))) + * + * RFC: https://datatracker.ietf.org/doc/html/rfc7636#section-4.2 + * @param {String} codeVerifier + * @returns {String} code challenge + */ +export async function createCodeChallenge(codeVerifier) { + // Generate SHA-256 digest of the [codeVerifier] + const buffer = new TextEncoder().encode(codeVerifier); + const digestArrayBuffer = await window.crypto.subtle.digest( + PKCE_CODE_CHALLENGE_DIGEST_ALGORITHM.long, + buffer, + ); + + // Convert digest to a Base64URL-encoded string + const digestHash = bufferToBase64(digestArrayBuffer); + // Escape string to remove reserved charaters + const codeChallenge = base64ToBase64Url(digestHash); + + return codeChallenge; +} diff --git a/app/assets/javascripts/lib/utils/accessor.js b/app/assets/javascripts/lib/utils/accessor.js index d4a6d70c62c..f7cdc564538 100644 --- a/app/assets/javascripts/lib/utils/accessor.js +++ b/app/assets/javascripts/lib/utils/accessor.js @@ -50,8 +50,16 @@ function canUseLocalStorage() { return safe; } +/** + * Determines if `window.crypto` is available. + */ +function canUseCrypto() { + return window.crypto?.subtle !== undefined; +} + const AccessorUtilities = { canUseLocalStorage, + canUseCrypto, }; export default AccessorUtilities; diff --git a/app/assets/javascripts/pages/jira_connect/oauth_callbacks/index.js b/app/assets/javascripts/pages/jira_connect/oauth_callbacks/index.js new file mode 100644 index 00000000000..3fe238dcb35 --- /dev/null +++ b/app/assets/javascripts/pages/jira_connect/oauth_callbacks/index.js @@ -0,0 +1,28 @@ +function getOriginURL() { + const origin = new URL(window.opener.location); + origin.hash = ''; + origin.search = ''; + + return origin; +} + +function postMessageToJiraConnectApp(data) { + window.opener.postMessage(data, getOriginURL().toString()); +} + +function initOAuthCallbacks() { + const params = new URLSearchParams(window.location.search); + if (params.has('code') && params.has('state')) { + postMessageToJiraConnectApp({ + success: true, + code: params.get('code'), + state: params.get('state'), + }); + } else { + postMessageToJiraConnectApp({ success: false }); + } + + window.close(); +} + +initOAuthCallbacks(); diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue index 73d75352cb5..5baeb309f79 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue @@ -21,7 +21,9 @@ export default { <gl-dropdown right text="Use an existing commit message" - variant="link" + category="tertiary" + variant="confirm" + size="small" class="mr-commit-dropdown" > <gl-dropdown-item diff --git a/app/controllers/concerns/spammable_actions/captcha_check/rest_api_actions_support.rb b/app/controllers/concerns/spammable_actions/captcha_check/rest_api_actions_support.rb new file mode 100644 index 00000000000..2ebfa90e6da --- /dev/null +++ b/app/controllers/concerns/spammable_actions/captcha_check/rest_api_actions_support.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +# This module should be included to support CAPTCHA check for REST API actions via Grape. +# +# If the request is directly handled by a controller action, then the corresponding module which +# supports HTML or JSON formats should be used instead. +module SpammableActions::CaptchaCheck::RestApiActionsSupport + extend ActiveSupport::Concern + include SpammableActions::CaptchaCheck::Common + include Spam::Concerns::HasSpamActionResponseFields + + private + + def with_captcha_check_rest_api(spammable:, &block) + # In the case of the REST API, the request is handled by Grape, so if there is a spam-related + # error, we don't render directly, instead we will pass the error message and other necessary + # fields to the Grape api error helper for it to handle. + captcha_render_lambda = -> do + fields = spam_action_response_fields(spammable) + + fields.delete :spam + # NOTE: "409 - Conflict" seems to be the most appropriate HTTP status code for a response + # which requires a CAPTCHA to be solved in order for the request to be resubmitted. + # https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.10 + status = 409 + + # NOTE: This nested 'error' key may not be consistent with all other API error responses, + # because they are not currently consistent across different API endpoints + # and models. Some (snippets) will nest errors in an errors key like this, + # while others (issues) will return the model's errors hash without an errors key, + # while still others just return a plain string error. + # See https://gitlab.com/groups/gitlab-org/-/epics/5527#revisit-inconsistent-shape-of-error-responses-in-rest-api + fields[:message] = { error: spammable.errors.full_messages.to_sentence } + render_structured_api_error!(fields, status) + end + + with_captcha_check_common(spammable: spammable, captcha_render_lambda: captcha_render_lambda, &block) + end +end diff --git a/app/controllers/jira_connect/oauth_callbacks_controller.rb b/app/controllers/jira_connect/oauth_callbacks_controller.rb new file mode 100644 index 00000000000..f603a563402 --- /dev/null +++ b/app/controllers/jira_connect/oauth_callbacks_controller.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +# This controller's role is to serve as a landing page +# that users get redirected to after installing and authenticating +# The GitLab.com for Jira App (https://marketplace.atlassian.com/apps/1221011/gitlab-com-for-jira-cloud) +# +class JiraConnect::OauthCallbacksController < ApplicationController + feature_category :integrations + + def index; end +end diff --git a/app/controllers/jira_connect/subscriptions_controller.rb b/app/controllers/jira_connect/subscriptions_controller.rb index fcd95c7942c..ec6ba07a125 100644 --- a/app/controllers/jira_connect/subscriptions_controller.rb +++ b/app/controllers/jira_connect/subscriptions_controller.rb @@ -16,6 +16,10 @@ class JiraConnect::SubscriptionsController < JiraConnect::ApplicationController p.style_src(*style_src_values) end + before_action do + push_frontend_feature_flag(:jira_connect_oauth, @user, default_enabled: :yaml) + end + before_action :allow_rendering_in_iframe, only: :index before_action :verify_qsh_claim!, only: :index before_action :authenticate_user!, only: :create diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index fb2fa547447..ba6c0380edf 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -178,7 +178,7 @@ module AuthHelper end def google_tag_manager_enabled? - return false unless Gitlab.dev_env_or_com? + return false unless Gitlab.com? if Feature.enabled?(:gtm_nonce, type: :ops) extra_config.has_key?('google_tag_manager_nonce_id') && diff --git a/app/helpers/jira_connect_helper.rb b/app/helpers/jira_connect_helper.rb index 9a0f0944fd1..67b85b26f9e 100644 --- a/app/helpers/jira_connect_helper.rb +++ b/app/helpers/jira_connect_helper.rb @@ -9,12 +9,38 @@ module JiraConnectHelper subscriptions: subscriptions.map { |s| serialize_subscription(s) }.to_json, subscriptions_path: jira_connect_subscriptions_path, users_path: current_user ? nil : jira_connect_users_path, # users_path is used to determine if user is signed in - gitlab_user_path: current_user ? user_path(current_user) : nil + gitlab_user_path: current_user ? user_path(current_user) : nil, + oauth_metadata: Feature.enabled?(:jira_connect_oauth, current_user) ? jira_connect_oauth_data.to_json : nil } end private + def jira_connect_oauth_data + oauth_authorize_url = oauth_authorization_url( + client_id: ENV['JIRA_CONNECT_OAUTH_CLIENT_ID'], + response_type: 'code', + scope: 'api', + redirect_uri: jira_connect_oauth_callbacks_url, + state: oauth_state + ) + + { + oauth_authorize_url: oauth_authorize_url, + oauth_token_url: oauth_token_url, + state: oauth_state, + oauth_token_payload: { + grant_type: :authorization_code, + client_id: ENV['JIRA_CONNECT_OAUTH_CLIENT_ID'], + redirect_uri: jira_connect_oauth_callbacks_url + } + } + end + + def oauth_state + @oauth_state ||= SecureRandom.hex(32) + end + def serialize_subscription(subscription) { group: { diff --git a/app/helpers/sessions_helper.rb b/app/helpers/sessions_helper.rb index e9466a9e97e..f0389000eb3 100644 --- a/app/helpers/sessions_helper.rb +++ b/app/helpers/sessions_helper.rb @@ -5,7 +5,7 @@ module SessionsHelper def recently_confirmed_com? strong_memoize(:recently_confirmed_com) do - ::Gitlab.dev_env_or_com? && + ::Gitlab.com? && !!flash[:notice]&.include?(t(:confirmed, scope: [:devise, :confirmations])) end end diff --git a/app/helpers/whats_new_helper.rb b/app/helpers/whats_new_helper.rb index ccccfcb930b..bbf56c51c6d 100644 --- a/app/helpers/whats_new_helper.rb +++ b/app/helpers/whats_new_helper.rb @@ -10,7 +10,7 @@ module WhatsNewHelper end def display_whats_new? - (Gitlab.dev_env_org_or_com? || user_signed_in?) && + (Gitlab.org_or_com? || user_signed_in?) && !Gitlab::CurrentSettings.current_application_settings.whats_new_variant_disabled? end diff --git a/app/models/integrations/base_chat_notification.rb b/app/models/integrations/base_chat_notification.rb index d0d54a92021..d5b6357cb66 100644 --- a/app/models/integrations/base_chat_notification.rb +++ b/app/models/integrations/base_chat_notification.rb @@ -241,7 +241,6 @@ module Integrations def notify_for_ref?(data) return true if data[:object_kind] == 'tag_push' - return true if data[:object_kind] == 'deployment' && !Feature.enabled?(:chat_notification_deployment_protected_branch_filter, project) ref = data[:ref] || data.dig(:object_attributes, :ref) return true if ref.blank? # No need to check protected branches when there is no ref diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb index 77897c5807f..f8e7a912896 100644 --- a/app/policies/base_policy.rb +++ b/app/policies/base_policy.rb @@ -67,7 +67,7 @@ class BasePolicy < DeclarativePolicy::Base rule { default }.enable :read_cross_project - condition(:is_gitlab_com, score: 0, scope: :global) { ::Gitlab.dev_env_or_com? } + condition(:is_gitlab_com, score: 0, scope: :global) { ::Gitlab.com? } end BasePolicy.prepend_mod_with('BasePolicy') diff --git a/app/services/spam/spam_params.rb b/app/services/spam/spam_params.rb index ccc17a42f01..81db6b390b2 100644 --- a/app/services/spam/spam_params.rb +++ b/app/services/spam/spam_params.rb @@ -25,6 +25,7 @@ module Spam # then the spam check may fail, or the SpamLog or UserAgentDetail may have missing fields. class SpamParams def self.new_from_request(request:) + self.normalize_grape_request_headers(request: request) self.new( captcha_response: request.headers['X-GitLab-Captcha-Response'], spam_log_id: request.headers['X-GitLab-Spam-Log-Id'], @@ -52,5 +53,14 @@ module Spam other.user_agent == user_agent && other.referer == referer end + + def self.normalize_grape_request_headers(request:) + # If needed, make a normalized copy of Grape headers with the case of 'GitLab' (with an + # uppercase 'L') instead of 'Gitlab' (with a lowercase 'l'), because Grape header helper keys + # are "coerced into a capitalized kebab case". See https://github.com/ruby-grape/grape#request + %w[X-Gitlab-Captcha-Response X-Gitlab-Spam-Log-Id].each do |header| + request.headers[header.gsub('Gitlab', 'GitLab')] = request.headers[header] if request.headers.key?(header) + end + end end end diff --git a/app/views/devise/shared/_email_opted_in.html.haml b/app/views/devise/shared/_email_opted_in.html.haml index 3817f9f651d..898b8f31f1d 100644 --- a/app/views/devise/shared/_email_opted_in.html.haml +++ b/app/views/devise/shared/_email_opted_in.html.haml @@ -1,4 +1,4 @@ -- return unless Gitlab.dev_env_or_com? +- return unless Gitlab.com? .gl-mb-3.js-email-opt-in.hidden .gl-font-weight-bold.gl-mb-3 diff --git a/app/views/devise/shared/_terms_of_service_notice.html.haml b/app/views/devise/shared/_terms_of_service_notice.html.haml index 75d567a03fd..1c6dc1f2d5d 100644 --- a/app/views/devise/shared/_terms_of_service_notice.html.haml +++ b/app/views/devise/shared/_terms_of_service_notice.html.haml @@ -1,7 +1,7 @@ - return unless Gitlab::CurrentSettings.current_application_settings.enforce_terms? %p.gl-text-gray-500.gl-mt-5.gl-mb-0 - - if Gitlab.dev_env_or_com? + - if Gitlab.com? = html_escape(s_("SignUp|By clicking %{button_text}, I agree that I have read and accepted the GitLab %{link_start}Terms of Use and Privacy Policy%{link_end}")) % { button_text: button_text, link_start: "<a href='#{terms_path}' target='_blank' rel='noreferrer noopener'>".html_safe, link_end: '</a>'.html_safe } - else diff --git a/app/views/jira_connect/oauth_callbacks/index.html.haml b/app/views/jira_connect/oauth_callbacks/index.html.haml new file mode 100644 index 00000000000..d35834bf05d --- /dev/null +++ b/app/views/jira_connect/oauth_callbacks/index.html.haml @@ -0,0 +1 @@ +%p= s_('Integrations|You can close this window.') |