You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

GitHubSettings.java 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2024 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. package org.sonar.auth.github;
  21. import com.google.common.annotations.VisibleForTesting;
  22. import java.util.Arrays;
  23. import java.util.List;
  24. import java.util.Set;
  25. import javax.annotation.CheckForNull;
  26. import javax.annotation.Nullable;
  27. import org.sonar.api.PropertyType;
  28. import org.sonar.api.ce.ComputeEngineSide;
  29. import org.sonar.api.config.Configuration;
  30. import org.sonar.api.config.PropertyDefinition;
  31. import org.sonar.api.server.ServerSide;
  32. import org.sonar.auth.DevOpsPlatformSettings;
  33. import org.sonar.db.DbClient;
  34. import org.sonar.db.DbSession;
  35. import org.sonar.db.alm.setting.ALM;
  36. import org.sonar.server.property.InternalProperties;
  37. import static java.lang.String.format;
  38. import static java.lang.String.valueOf;
  39. import static org.apache.commons.lang3.StringUtils.isNotBlank;
  40. import static org.sonar.api.PropertyType.BOOLEAN;
  41. import static org.sonar.api.PropertyType.PASSWORD;
  42. import static org.sonar.api.PropertyType.STRING;
  43. import static org.sonar.api.utils.Preconditions.checkState;
  44. @ServerSide
  45. @ComputeEngineSide
  46. public class GitHubSettings implements DevOpsPlatformSettings {
  47. public static final String CLIENT_ID = "sonar.auth.github.clientId.secured";
  48. public static final String CLIENT_SECRET = "sonar.auth.github.clientSecret.secured";
  49. public static final String APP_ID = "sonar.auth.github.appId";
  50. public static final String PRIVATE_KEY = "sonar.auth.github.privateKey.secured";
  51. public static final String ENABLED = "sonar.auth.github.enabled";
  52. public static final String ALLOW_USERS_TO_SIGN_UP = "sonar.auth.github.allowUsersToSignUp";
  53. public static final String GROUPS_SYNC = "sonar.auth.github.groupsSync";
  54. public static final String API_URL = "sonar.auth.github.apiUrl";
  55. public static final String DEFAULT_API_URL = "https://api.github.com/";
  56. public static final String WEB_URL = "sonar.auth.github.webUrl";
  57. public static final String DEFAULT_WEB_URL = "https://github.com/";
  58. public static final String ORGANIZATIONS = "sonar.auth.github.organizations";
  59. @VisibleForTesting
  60. static final String PROVISIONING = "provisioning.github.enabled";
  61. @VisibleForTesting
  62. static final String PROVISION_VISIBILITY = "provisioning.github.project.visibility.enabled";
  63. @VisibleForTesting
  64. static final String USER_CONSENT_FOR_PERMISSIONS_REQUIRED_AFTER_UPGRADE = "sonar.auth.github.userConsentForPermissionProvisioningRequired";
  65. private static final String CATEGORY = "authentication";
  66. private static final String SUBCATEGORY = "github";
  67. private final Configuration configuration;
  68. private final InternalProperties internalProperties;
  69. private final DbClient dbClient;
  70. public GitHubSettings(Configuration configuration, InternalProperties internalProperties, DbClient dbClient) {
  71. this.configuration = configuration;
  72. this.internalProperties = internalProperties;
  73. this.dbClient = dbClient;
  74. }
  75. public String clientId() {
  76. return configuration.get(CLIENT_ID).orElse("");
  77. }
  78. public String clientSecret() {
  79. return configuration.get(CLIENT_SECRET).orElse("");
  80. }
  81. public String appId() {
  82. return configuration.get(APP_ID).orElse("");
  83. }
  84. public String privateKey() {
  85. return configuration.get(PRIVATE_KEY).orElse("");
  86. }
  87. public boolean isEnabled() {
  88. return configuration.getBoolean(ENABLED).orElse(false) && !clientId().isEmpty() && !clientSecret().isEmpty();
  89. }
  90. public boolean allowUsersToSignUp() {
  91. return configuration.getBoolean(ALLOW_USERS_TO_SIGN_UP).orElse(false);
  92. }
  93. public boolean syncGroups() {
  94. return configuration.getBoolean(GROUPS_SYNC).orElse(false);
  95. }
  96. @CheckForNull
  97. String webURL() {
  98. return urlWithEndingSlash(configuration.get(WEB_URL).orElse(""));
  99. }
  100. @CheckForNull
  101. public String apiURL() {
  102. return urlWithEndingSlash(configuration.get(API_URL).orElse(""));
  103. }
  104. public String apiURLOrDefault() {
  105. return configuration.get(API_URL).map(GitHubSettings::urlWithEndingSlash).orElse(DEFAULT_API_URL);
  106. }
  107. public Set<String> getOrganizations() {
  108. return Set.of(configuration.getStringArray(ORGANIZATIONS));
  109. }
  110. @CheckForNull
  111. private static String urlWithEndingSlash(@Nullable String url) {
  112. if (url != null && !url.endsWith("/")) {
  113. return url + "/";
  114. }
  115. return url;
  116. }
  117. public void setProvisioning(boolean enableProvisioning) {
  118. if (enableProvisioning) {
  119. checkGithubConfigIsCompleteForProvisioning();
  120. } else {
  121. removeExternalGroupsForGithub();
  122. }
  123. internalProperties.write(PROVISIONING, String.valueOf(enableProvisioning));
  124. }
  125. private void removeExternalGroupsForGithub() {
  126. try (DbSession dbSession = dbClient.openSession(false)) {
  127. dbClient.externalGroupDao().deleteByExternalIdentityProvider(dbSession, GitHubIdentityProvider.KEY);
  128. dbClient.githubOrganizationGroupDao().deleteAll(dbSession);
  129. dbSession.commit();
  130. }
  131. }
  132. private void checkGithubConfigIsCompleteForProvisioning() {
  133. checkState(isEnabled(), getErrorMessage("GitHub authentication must be enabled"));
  134. checkState(isNotBlank(appId()), getErrorMessage("Application ID must be provided"));
  135. checkState(isNotBlank(privateKey()), getErrorMessage("Private key must be provided"));
  136. }
  137. private static String getErrorMessage(String prefix) {
  138. return format("%s to enable GitHub provisioning.", prefix);
  139. }
  140. @Override
  141. public String getDevOpsPlatform() {
  142. return ALM.GITHUB.getId();
  143. }
  144. @Override
  145. public boolean isProvisioningEnabled() {
  146. return isEnabled() && internalProperties.read(PROVISIONING).map(Boolean::parseBoolean).orElse(false);
  147. }
  148. public boolean isUserConsentRequiredAfterUpgrade() {
  149. return configuration.get(USER_CONSENT_FOR_PERMISSIONS_REQUIRED_AFTER_UPGRADE).isPresent();
  150. }
  151. @Override
  152. public boolean isProjectVisibilitySynchronizationActivated() {
  153. return configuration.getBoolean(PROVISION_VISIBILITY).orElse(true);
  154. }
  155. public static List<PropertyDefinition> definitions() {
  156. int index = 1;
  157. return Arrays.asList(
  158. PropertyDefinition.builder(ENABLED)
  159. .name("Enabled")
  160. .description("Enable GitHub users to login. Value is ignored if client ID and secret are not defined.")
  161. .category(CATEGORY)
  162. .subCategory(SUBCATEGORY)
  163. .type(BOOLEAN)
  164. .defaultValue(valueOf(false))
  165. .index(index++)
  166. .build(),
  167. PropertyDefinition.builder(CLIENT_ID)
  168. .name("Client ID")
  169. .description("Client ID provided by GitHub when registering the application.")
  170. .category(CATEGORY)
  171. .subCategory(SUBCATEGORY)
  172. .index(index++)
  173. .build(),
  174. PropertyDefinition.builder(CLIENT_SECRET)
  175. .name("Client Secret")
  176. .description("Client password provided by GitHub when registering the application.")
  177. .category(CATEGORY)
  178. .subCategory(SUBCATEGORY)
  179. .type(PASSWORD)
  180. .index(index++)
  181. .build(),
  182. PropertyDefinition.builder(APP_ID)
  183. .name("App ID")
  184. .description("The App ID is found on your GitHub App's page on GitHub at Settings > Developer Settings > GitHub Apps.")
  185. .category(CATEGORY)
  186. .subCategory(SUBCATEGORY)
  187. .type(STRING)
  188. .index(index++)
  189. .build(),
  190. PropertyDefinition.builder(PRIVATE_KEY)
  191. .name("Private Key")
  192. .description("""
  193. Your GitHub App's private key. You can generate a .pem file from your GitHub App's page under Private keys.
  194. Copy and paste the whole contents of the file here.""")
  195. .category(CATEGORY)
  196. .subCategory(SUBCATEGORY)
  197. .type(PropertyType.TEXT)
  198. .index(index++)
  199. .build(),
  200. PropertyDefinition.builder(ALLOW_USERS_TO_SIGN_UP)
  201. .name("Allow users to sign up")
  202. .description("Allow new users to authenticate. When set to disabled, only existing users will be able to authenticate to the server.")
  203. .category(CATEGORY)
  204. .subCategory(SUBCATEGORY)
  205. .type(BOOLEAN)
  206. .defaultValue(valueOf(true))
  207. .index(index++)
  208. .build(),
  209. PropertyDefinition.builder(GROUPS_SYNC)
  210. .name("Synchronize teams as groups")
  211. .description("Synchronize GitHub team with SonarQube group memberships when users log in to SonarQube."
  212. + " For each GitHub team they belong to, users will be associated to a group of the same name if it exists in SonarQube.")
  213. .category(CATEGORY)
  214. .subCategory(SUBCATEGORY)
  215. .type(BOOLEAN)
  216. .defaultValue(valueOf(false))
  217. .index(index++)
  218. .build(),
  219. PropertyDefinition.builder(API_URL)
  220. .name("The API url for a GitHub instance.")
  221. .description(String.format("The API url for a GitHub instance. %s for Github.com, https://github.company.com/api/v3/ when using Github Enterprise", DEFAULT_API_URL))
  222. .category(CATEGORY)
  223. .subCategory(SUBCATEGORY)
  224. .type(STRING)
  225. .defaultValue(DEFAULT_API_URL)
  226. .index(index++)
  227. .build(),
  228. PropertyDefinition.builder(WEB_URL)
  229. .name("The WEB url for a GitHub instance.")
  230. .description(String.format("The WEB url for a GitHub instance. %s for Github.com, https://github.company.com/ when using GitHub Enterprise.", DEFAULT_WEB_URL))
  231. .category(CATEGORY)
  232. .subCategory(SUBCATEGORY)
  233. .type(STRING)
  234. .defaultValue(DEFAULT_WEB_URL)
  235. .index(index++)
  236. .build(),
  237. PropertyDefinition.builder(ORGANIZATIONS)
  238. .name("Organizations")
  239. .description("Only members of these organizations will be able to authenticate to the server. "
  240. + "⚠ if not set, users from any organization where the GitHub App is installed will be able to login to this SonarQube instance.")
  241. .multiValues(true)
  242. .category(CATEGORY)
  243. .subCategory(SUBCATEGORY)
  244. .index(index)
  245. .build(),
  246. PropertyDefinition.builder(PROVISION_VISIBILITY)
  247. .name("Provision project visibility")
  248. .description("Change project visibility based on GitHub repository visibility. If disabled, every provisioned project will be private in SonarQube and visible only"
  249. + " to users with explicit GitHub permissions for the corresponding repository. Changes take effect at the next synchronization.")
  250. .type(BOOLEAN)
  251. .category(CATEGORY)
  252. .subCategory(SUBCATEGORY)
  253. .defaultValue(valueOf(true))
  254. .index(index)
  255. .build());
  256. }
  257. }