Skip to content

Commit

Permalink
Fix #2391 Add authorization capability to project creator
Browse files Browse the repository at this point in the history
Supports in-IDE credentials, and sourcing from Maven's settings.xml and Gradle's gradle.properties
  • Loading branch information
RedNesto committed Nov 28, 2024
1 parent b2db9e3 commit fed7233
Show file tree
Hide file tree
Showing 9 changed files with 688 additions and 30 deletions.
17 changes: 14 additions & 3 deletions src/main/kotlin/MinecraftConfigurable.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ package com.demonwav.mcdev

import com.demonwav.mcdev.asset.MCDevBundle
import com.demonwav.mcdev.asset.PlatformAssets
import com.demonwav.mcdev.creator.custom.mavenRepoTable
import com.demonwav.mcdev.creator.custom.templateRepoTable
import com.demonwav.mcdev.update.ConfigurePluginUpdatesDialog
import com.intellij.ide.projectView.ProjectView
Expand Down Expand Up @@ -94,16 +95,26 @@ class MinecraftConfigurable : Configurable {
}

group(MCDevBundle("minecraft.settings.creator")) {
row(MCDevBundle("minecraft.settings.creator.repos")) {}
twoColumnsRow(
{ label(MCDevBundle("minecraft.settings.creator.repos")) },
{ label(MCDevBundle("minecraft.settings.creator.maven")) },
)

row {
twoColumnsRow({
templateRepoTable(
MutableProperty(
{ settings.creatorTemplateRepos.toMutableList() },
{ settings.creatorTemplateRepos = it }
)
)
}.resizableRow()
}, {
mavenRepoTable(
MutableProperty(
{ settings.creatorMavenRepos.toMutableList() },
{ settings.creatorMavenRepos = it }
)
)
}).resizableRow()
}

onApply {
Expand Down
22 changes: 22 additions & 0 deletions src/main/kotlin/MinecraftSettings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import com.intellij.openapi.editor.markup.EffectType
import com.intellij.util.xmlb.annotations.Attribute
import com.intellij.util.xmlb.annotations.Tag
import com.intellij.util.xmlb.annotations.Text
import com.intellij.util.xmlb.annotations.Transient

@State(name = "MinecraftSettings", storages = [Storage("minecraft_dev.xml")])
class MinecraftSettings : PersistentStateComponent<MinecraftSettings.State> {
Expand All @@ -43,6 +44,7 @@ class MinecraftSettings : PersistentStateComponent<MinecraftSettings.State> {
var mixinClassIcon: Boolean = true,

var creatorTemplateRepos: List<TemplateRepo> = listOf(TemplateRepo.makeBuiltinRepo()),
var creatorMavenRepos: List<MavenRepo> = listOf(),
)

@Tag("repo")
Expand All @@ -64,6 +66,20 @@ class MinecraftSettings : PersistentStateComponent<MinecraftSettings.State> {
}
}

@Tag("maven")
data class MavenRepo(
@get:Attribute("id")
var id: String,
@get:Attribute("url")
var url: String,
@get:Attribute("username")
var username: String,
@get:Transient // Saved in PasswordSafe
var password: String?
) {
constructor() : this("", "", "", "")
}

private var state = State()

override fun getState(): State {
Expand Down Expand Up @@ -120,6 +136,12 @@ class MinecraftSettings : PersistentStateComponent<MinecraftSettings.State> {
state.creatorTemplateRepos = creatorTemplateRepos.map { it.copy() }
}

var creatorMavenRepos: List<MavenRepo>
get() = state.creatorMavenRepos.map { it.copy() }
set(creatorMavenRepos) {
state.creatorMavenRepos = creatorMavenRepos.map { it.copy() }
}

enum class UnderlineType(private val regular: String, val effectType: EffectType) {

NORMAL("Normal", EffectType.LINE_UNDERSCORE),
Expand Down
222 changes: 222 additions & 0 deletions src/main/kotlin/creator/custom/CreatorCredentials.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
/*
* Minecraft Development for IntelliJ
*
* https://mcdev.io/
*
* Copyright (C) 2024 minecraft-dev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published
* by the Free Software Foundation, version 3.0 only.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package com.demonwav.mcdev.creator.custom

import com.demonwav.mcdev.MinecraftSettings
import com.demonwav.mcdev.creator.custom.providers.RemoteTemplateProvider.RemoteAuthType
import com.github.kittinunf.fuel.core.Request
import com.github.kittinunf.fuel.core.extensions.authentication
import com.intellij.collaboration.auth.ServerAccount
import com.intellij.collaboration.auth.findAccountOrNull
import com.intellij.credentialStore.CredentialAttributes
import com.intellij.credentialStore.Credentials
import com.intellij.credentialStore.generateServiceName
import com.intellij.ide.passwordSafe.PasswordSafe
import git4idea.remote.GitHttpAuthDataProvider
import git4idea.remote.hosting.http.SilentHostedGitHttpAuthDataProvider
import java.nio.file.Path
import java.util.Properties
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.xpath.XPathFactory
import javax.xml.xpath.XPathNodes
import kotlin.io.path.Path
import kotlin.io.path.exists
import kotlin.io.path.inputStream

object CreatorCredentials {

private val xmlDocumentBuilder = DocumentBuilderFactory.newDefaultInstance().newDocumentBuilder()
private val xPath = XPathFactory.newDefaultInstance().newXPath()

private fun makeServiceName(url: String): String = generateServiceName("MinecraftDev Creator", url)

fun persistCredentials(
url: String,
username: String?,
password: String,
) {
val serviceName = makeServiceName(url)
val credAttrs = CredentialAttributes(serviceName, username)
PasswordSafe.instance.setPassword(credAttrs, password)
}

fun getCredentials(url: String, username: String?): Credentials? {
val serviceName = makeServiceName(url)
val credAttrs = CredentialAttributes(serviceName, username)
return PasswordSafe.instance[credAttrs]
}

suspend fun configureAuthorization(
request: Request,
url: String,
authType: RemoteAuthType,
credentials: String
): Request {
when (authType) {
RemoteAuthType.NONE -> return request

RemoteAuthType.BASIC -> {
val creds = getCredentials(url, credentials)
val username = creds?.userName
val password = creds?.getPasswordAsString()
if (username != null && password != null) {
return request.authentication().basic(username, password)
}
}

RemoteAuthType.BEARER -> {
val creds = getCredentials(url, null)
val password = creds?.getPasswordAsString()
if (password != null) {
return request.authentication().bearer(password)
}
}

RemoteAuthType.GIT_HTTP -> {
val creds = findGitHttpAuthBearerToken(credentials)
if (creds != null) {
return request.authentication().basic(creds.first, creds.second)
}
}

RemoteAuthType.HEADER -> {
val creds = getCredentials(url, credentials)
val username = creds?.userName
val password = creds?.getPasswordAsString()
if (username != null && password != null) {
return request.header(username, password)
}
}
}

return request
}

fun getGitHttpAuthProviders(): List<SilentHostedGitHttpAuthDataProvider<ServerAccount>> {
return GitHttpAuthDataProvider.EP_NAME.extensionList
.filterIsInstance<SilentHostedGitHttpAuthDataProvider<ServerAccount>>()
}

fun findGitHttpAuthProvider(providerId: String): SilentHostedGitHttpAuthDataProvider<ServerAccount>? {
return getGitHttpAuthProviders().find { provider -> provider.providerId == providerId }
}

fun getGitHttpAuthAccounts(providerId: String): MutableList<ServerAccount> {
return findGitHttpAuthProvider(providerId)?.accountManager?.accountsState?.value.orEmpty().toMutableList()
}

fun findGitHttpAuthAccount(credentials: String): ServerAccount? {
val providerId = credentials.substringBefore(':').takeIf(String::isNotBlank) ?: return null
val accountId = credentials.substringAfter(':').takeIf(String::isNotBlank) ?: return null
val accountManager = findGitHttpAuthProvider(providerId)?.accountManager ?: return null
return accountManager.findAccountOrNull { account -> account.id == accountId }
}

suspend fun findGitHttpAuthBearerToken(credentials: String): Pair<String, String>? {
val providerId = credentials.substringBefore(':').takeIf(String::isNotBlank) ?: return null
val accountId = credentials.substringAfter(':').takeIf(String::isNotBlank) ?: return null
val accountManager = findGitHttpAuthProvider(providerId)?.accountManager ?: return null
val account = accountManager.findAccountOrNull { account -> account.id == accountId } ?: return null
val token = accountManager.findCredentials(account) ?: return null
return account.name to token
}

fun findMavenRepoCredentials(url: String): Pair<String, String>? {
val repoData = MinecraftSettings.instance.creatorMavenRepos.find { url.startsWith(it.url) }
if (repoData == null) {
return null
}

// First check credentials in IntelliJ IDEA
if (repoData.username.isNotBlank()) {
val credentials = getCredentials(repoData.url, repoData.username)
var username = credentials?.userName
var password = credentials?.getPasswordAsString()
if (username != null && password != null) {
return username to password
}
}

// If IntelliJ doesn't have them look into the Maven settings, or Gradle properties
val sourcedCredentials = findMavenServerCredentials(repoData.id) ?: findGradleRepoCredentials(repoData.id)
if (sourcedCredentials != null) {
return sourcedCredentials
}

return null
}

fun getMavenSettingsPath(): Path {
return Path(System.getProperty("user.home"), ".m2", "settings.xml")
}

private fun findMavenServerCredentials(serverId: String): Pair<String, String>? {
val path = getMavenSettingsPath()
if (!path.exists()) {
return null
}

val document = path.inputStream().use { input -> xmlDocumentBuilder.parse(input) }
val nodes = xPath.evaluateExpression(
"/settings/servers/server/id/text()[.=\"$serverId\"]/ancestor::server/*",
document,
XPathNodes::class.java
)

var username: String? = null
var password: String? = null
for (node in nodes) {
when (node.nodeName) {
"username" -> username = node.textContent
"password" -> password = node.textContent
}
}

if (username != null && password != null) {
return username to password
}

return null
}

fun getGradleProperties(): Path {
return System.getenv("GRADLE_USER_HOME")?.let(::Path)
?: Path(System.getProperty("user.home"), ".gradle", "gradle.properties")
}

private fun findGradleRepoCredentials(id: String): Pair<String, String>? {
val path = getGradleProperties()
if (!path.exists()) {
return null
}

val properties = Properties()
path.inputStream().use(properties::load)

val username = properties[id + "Username"]?.toString()
val password = properties[id + "Password"]?.toString()
if (username != null && password != null) {
return username to password
}

return null
}
}
Loading

0 comments on commit fed7233

Please sign in to comment.