diff --git a/.gitignore b/.gitignore index eedcf4e4..51f80ebc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ .vscode/ .gitattributes -*.code-workspace \ No newline at end of file +*.code-workspace + +# using TestClass.cls for objecscript functionality exploration / convince myself that things work the way I expect them to +cls/SourceControl/Git/TestClass.cls \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 98c16142..726ab243 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Partial support for production decomposition with the new interoperability editors - Added Lock Branch setting to prevent switching branches for a protected namespace (#709) - Tooltips on branch operations in Git UI (#725) +- Support for https connections (#279) ### Fixed - Changing system mode (environment name) in settings persists after instance restart (#655) diff --git a/README.md b/README.md index ec99d771..c32845c9 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Embedded Git support for InterSystems platforms, supporting unified source contr ``` d ##class(SourceControl.Git.API).Configure() ``` - This will also allow you to generate an SSH key for use as (e.g.) a deploy key and to initialize or clone a git repo. + This will also allow you to generate an SSH key for use as (e.g.) a deploy key and to initialize or clone a git repo. (If you want to use https instead, please see the documentation [here])(/docs/https.md) 3. If using VSCode: Set up `isfs` server-side editing. First, save your current workspace in which you have the code open. Then, open the `.code-workspace` file generated by VS Code and add the following to the list of folders: ``` { @@ -150,6 +150,9 @@ Assuming you have the local and remote repositories created, `git config core.sshCommand 'ssh -i ~/.ssh/'` 8. Test the refresh button for the remote branches on the WebUI, fetch from the source control menu in Studio or VS Code, and `git fetch` in Git Bash. All 3 should work without any issues. +### HTTPS Support +We recommend that people connect to their remote git repository using SSH. If you cannot use SSH connections, we also have support for HTTPS connection through OAuth2. See [our documentation for setting up an https connection](/docs/https.md). + ## Support If you find a bug or would like to request an enhancement, [report an issue](https://github.com/intersystems/git-source-control/issues/new). If you have a question, post it on the [InterSystems Developer Community](https://community.intersystems.com/) - consider using the "Git" and "Source Control" tags as appropriate. diff --git a/cls/SourceControl/Git/Extension.cls b/cls/SourceControl/Git/Extension.cls index 77ebe01a..73a673a6 100644 --- a/cls/SourceControl/Git/Extension.cls +++ b/cls/SourceControl/Git/Extension.cls @@ -11,6 +11,7 @@ XData Menu + @@ -193,6 +194,7 @@ Method OnSourceMenuItem(name As %String, ByRef Enabled As %String, ByRef Display set Enabled = $CASE(name, "Status": 1, "GitWebUI" : 1, + "Authenticate":1, "Import": 1, "ImportForce": 1, "NewBranch": BranchLocked, @@ -206,6 +208,7 @@ Method OnSourceMenuItem(name As %String, ByRef Enabled As %String, ByRef Display // cases "Status": 1, "GitWebUI" : 1, + "Authenticate" : 1, "Export": 1, "ExportForce": 1, "Import": 1, diff --git a/cls/SourceControl/Git/OAuth2.cls b/cls/SourceControl/Git/OAuth2.cls new file mode 100644 index 00000000..8b296679 --- /dev/null +++ b/cls/SourceControl/Git/OAuth2.cls @@ -0,0 +1,59 @@ +Include %syPrompt + +IncludeGenerator %syPrompt + +Class SourceControl.Git.OAuth2 Extends %RegisteredObject +{ + +/// GenerateVerifier returns a cryptographically random 32 byte value +ClassMethod GenerateVerifier() As %String +{ + new $NAMESPACE + set $NAMESPACE = "%SYS" + return ##class(%SYSTEM.Encryption).GenCryptRand(32) +} + +/// Builds the authorization code URL for the given configuration +ClassMethod AuthCodeURL(c As SourceControl.Git.OAuth2.Config, namespace As %String, Output state, Output verifier) As %String +{ + set state = namespace_"_"_..GenerateVerifier() + set verifier = ..GenerateVerifier() + set url = c.AuthCodeURL(state, verifier) + return url +} + +ClassMethod GetURLsFromRemote(remote As %String, Output authCodeURL, Output tokenURL) As %Boolean +{ + if remote [ "github.com/" { + set authCodeURL = "https://github.com/login/oauth/authorize" + set tokenURL = "https://github.com/login/oauth/access_token" + return 1 + } elseif remote [ "gitlab" { + set gitlaburl = $Piece(remote, ".com", 1) _ ".com/" + set authCodeURL = gitlaburl _ "/oauth/authorize" + set tokenUTL = gitlaburl _ "/oauth/token" + return 1 + } else { + return 0 + } +} + +ClassMethod FixRemoteURL(url As %String) As %String +{ + /// OAuth Https authentication requires connecting as api + if ($extract(url,1,5) = "https") { + if (url [ "@") { + return url + } else { + set url = "https://api@"_$piece(url,"https://",2) + } + } + return url +} + +ClassMethod GetToken() As %String +{ + return ##class(SourceControl.Git.Util.CredentialManager).GetToken($username, .err, .code) +} + +} diff --git a/cls/SourceControl/Git/OAuth2/Config.cls b/cls/SourceControl/Git/OAuth2/Config.cls new file mode 100644 index 00000000..d0d858e5 --- /dev/null +++ b/cls/SourceControl/Git/OAuth2/Config.cls @@ -0,0 +1,227 @@ +Class SourceControl.Git.OAuth2.Config Extends %Persistent +{ + +/// Name is the identifier for this configuration +Property Name As %String(MAXLEN = 127); + +/// ClientID is the OAuth Application ID. Stored in private memopry store only accessible by user +Property ClientID As %String(MAXLEN = "") [ Transient ]; + +/// ClientSecret is the OAuth Application secret. Stored in private memopry store only accessible by user +Property ClientSecret As %String(MAXLEN = "") [ Transient ]; + +/// Endpoint contains the resource server's token endpoint +Property Endpoint As Endpoint; + +/// RedirectURL is the URL to redirect the auth token +/// to after authenticating with the resource owner +Property RedirectURL As %String(MAXLEN = ""); + +Property state As %String; + +Property verifier As %String; + +/// Scopes specifies the list of scopes we are requesting access to +Property Scopes As %List; + +Property Username As %String; + +// Using a direct input of authentication token as opposed to ClientId and ClientToken + +Property directToken As %String; + +Index Username On Username [ IdKey, Unique ]; + +Method ClientIDSet(InputValue As %String) As %Status +{ + set code = "", error = "" + do ##class(SourceControl.Git.Util.CredentialManager).SetKeyPair(..Username,"clientid",InputValue, .error, .code) + if (code '= 1) || (error '= "") { + return $$$ERROR($$$GeneralError,"Set failed with following error: "_error) + } + return $$$OK +} + +Method ClientIDGet() As %String +{ + return ##class(SourceControl.Git.Util.CredentialManager).GetKeyPair(..Username,"clientid", .error, .code) +} + +Method ClientSecretSet(InputValue As %String) As %Status +{ + set code = "", error = "" + do ##class(SourceControl.Git.Util.CredentialManager).SetKeyPair(..Username,"clientsecret",InputValue, .error, .code) + if (code '= 1) || (error '= "") { + return $$$ERROR($$$GeneralError,"Set failed with following error: "_error) + } + return $$$OK +} + +Method ClientSecretGet() As %String +{ + return ##class(SourceControl.Git.Util.CredentialManager).GetKeyPair(..Username,"clientsecret", .error, .code) +} + +ClassMethod GetConfig(username As %String) As SourceControl.Git.OAuth2.Config +{ + set config = ##class(SourceControl.Git.OAuth2.Config).%OpenId(username) + + return config +} + +// TODO: We will need a authStyleCache when we use autodetect for Endpoint.AuthStyle in the future + +Method %OnNew(configName As %String, clientID As %String, clientSecret As %String, authEndpoint As %String, tokenEndpoint As %String, redirectURL As %String, scopes As %List = "") As %Status +{ + set ..Name = configName + set ..Username = $username + set ..ClientID = clientID + set ..ClientSecret = clientSecret + set ..Endpoint = ##class(Endpoint).%New() + set ..Endpoint.AuthURL = authEndpoint + set ..Endpoint.TokenURL = tokenEndpoint + set ..RedirectURL = redirectURL + set ..directToken = 0 + + + if ('scopes) { + set scopes = $lb("repo") + } + set ..Scopes = scopes + + return $$$OK +} + +Method AuthCodeURL(state As %String, verifier As %String) As %String +{ + #; new $NAMESPACE + #; set $NAMESPACE = "%SYS" + + set params("response_type") = "code" + set params("client_id") = ..ClientID + set:(..RedirectURL '= "") params("redirect_uri") = ..RedirectURL + set:(state '= "") params("state") = state + set:($LISTLENGTH(..Scopes) > 0) params("scope") = $LISTTOSTRING(..Scopes," ") + if verifier { + set code = ##class(%SYSTEM.Encryption).SHAHash(256, verifier) + set params("code_challenge_method") = "S256" + set params("code_challenge") = code + } + + return ..GetURLWithParams(..Endpoint.AuthURL, .params) +} + +Method Exchange(authCode As %String, verifier As %String, Output sc As %Status) As %String +{ + do ##class(%Net.URLParser).Decompose(..Endpoint.TokenURL, .urlComponents) + + set request = ##class(%Net.HttpRequest).%New() + set request.Server = urlComponents("host") + set request.Https = (urlComponents("scheme")="https") + do request.SetParam("grant_type", "authorization_code") + do request.SetParam("code", authCode) + do request.SetParam("code_verifier", verifier) + do:(..ClientID '= "") request.SetParam("client_id", ..ClientID) + do:(..ClientSecret '= "") request.SetParam("client_secret", ..ClientSecret) + // we don't need the redirect_uri parameter because we will be consuming the token here + + do request.SetHeader("Accept", "application/json") + + do ..CreateSSLConfigIfNonExistent("GitExtensionForIris") + + set request.SSLConfiguration = "GitExtensionForIris" + set sc = request.Get(urlComponents("path")) + if sc '= $$$OK { + // something went wrong + return "" + } + + try { + set obj = {}.%FromJSON(request.HttpResponse.Data) + } catch ex { + set sc = ex.AsStatus() + return "" + } + + if obj.%IsDefined("access_token") && (obj.%GetTypeOf("access_token") = "string") { + return obj.%Get("access_token") + } else { + set sc = $$$ERROR($$$GeneralError,"Unable to read access_token from response") + return "" + } +} + +ClassMethod CreateSSLConfigIfNonExistent(name As %String) +{ + do ##class(%zpkg.isc.sc.git.SSLConfig).CreateSSLConfigIfNonExistent(name) +} + +ClassMethod GetURLWithParams(url As %String, ByRef params As %String) As %String +{ + if $find(url, "?") { + set url = url_"&" + } else { + set url = url_"?" + } + + set curParamKey = "" + for { + set isFirstIter = (curParamKey = "") + set curParamKey = $order(params(curParamKey), 1, curParamValue) + + set isLastIter = (curParamKey = "") + set:'(isFirstIter || isLastIter) url = url_"&" + + quit:(isLastIter) + + set url = url_$$$URLENCODE(curParamKey)_"="_$$$URLENCODE(curParamValue) + } + return url +} + +Storage Default +{ + + +%%CLASSNAME + + +ClientID + + +ClientSecret + + +Endpoint + + +RedirectURL + + +Scopes + + +Username + + +state + + +verifier + + +Name + + +directToken + + +^SourceControl.Git.O7826.ConfigD +ConfigDefaultData +^SourceControl.Git.O7826.ConfigD +^SourceControl.Git.O7826.ConfigI +^SourceControl.Git.O7826.ConfigS +%Storage.Persistent +} + +} diff --git a/cls/SourceControl/Git/OAuth2/Endpoint.cls b/cls/SourceControl/Git/OAuth2/Endpoint.cls new file mode 100644 index 00000000..b45a0162 --- /dev/null +++ b/cls/SourceControl/Git/OAuth2/Endpoint.cls @@ -0,0 +1,29 @@ +Class SourceControl.Git.OAuth2.Endpoint Extends %SerialObject +{ + +Property AuthURL As %String; + +Property DeviceAuthURL As %String; + +Property TokenURL As %String; + + +Storage Default +{ + + +AuthURL + + +DeviceAuthURL + + +TokenURL + + +EndpointState +^SourceControl.Git7826.EndpointS +%Storage.Serial +} + +} diff --git a/cls/SourceControl/Git/Settings.cls b/cls/SourceControl/Git/Settings.cls index 5b5f107d..a4d566c1 100644 --- a/cls/SourceControl/Git/Settings.cls +++ b/cls/SourceControl/Git/Settings.cls @@ -35,6 +35,12 @@ Property gitUserName As %String(MAXLEN = 255) [ InitialExpression = {##class(Sou /// Attribution: Email address for user ${username} Property gitUserEmail As %String(MAXLEN = 255) [ InitialExpression = {##class(SourceControl.Git.Utils).GitUserEmail()} ]; +/// URL for git remote +Property gitRemoteURL As %String(MAXLEN = "") [ InitialExpression = {##class(SourceControl.Git.Utils).GetConfiguredRemote()} ]; + +/// Type of git remote (SSH or HTTPS (Only with OAuth)) +Property gitRemoteType As %String(VALUELIST = ",HTTPS,SSH") [ InitialExpression = {##class(SourceControl.Git.Settings).GetRemoteType(##class(SourceControl.Git.Utils).GetConfiguredRemote())} ]; + /// Whether mapped items should be read-only, preventing them from being added to source control Property mappedItemsReadOnly As %Boolean [ InitialExpression = {##class(SourceControl.Git.Utils).MappedItemsReadOnly()} ]; @@ -65,6 +71,9 @@ Property environmentName As %String(MAXLEN = "") [ InitialExpression = {##class( /// Whether the branch should or should not be locked down from changing namespaces Property lockBranch As %Boolean [ InitialExpression = {##class(SourceControl.Git.Utils).LockBranch()} ]; +/// Whether we are using OAuth for https +Property OAuth As %Boolean [ InitialExpression = {##class(SourceControl.Git.Utils).UsingOAuth()} ]; + /// (Optional) A namespace-specific string that may be included in mapping configurations as to support multi-namespace repositories Property mappingsToken As %String(MAXLEN = "") [ InitialExpression = {##class(SourceControl.Git.Utils).MappingsToken()} ]; @@ -159,8 +168,18 @@ Method %Save() As %Status set @storage@("settings", "warnInstanceWideUncommitted") = ..warnInstanceWideUncommitted set @storage@("settings", "basicMode") = ..systemBasicMode set @storage@("settings", "environmentName") = ..environmentName + set @storage@("settings","gitRemoteType") = ..gitRemoteType + set @storage@("settings","gitRemoteURL") = ..gitRemoteURL set @storage@("settings", "lockBranch") = ..lockBranch + + if (..gitRemoteType = "HTTPS") { + set @storage@("settings","OAuth") = ..OAuth + } else { + set @storage@("settings","OAuth") = 0 + } + set @storage@("settings", "mappingsToken") = ..mappingsToken + if ..basicMode = "system" { kill @storage@("settings", "user", $username, "basicMode") } else { @@ -307,6 +326,13 @@ ClassMethod Configure() As %Boolean [ CodeMode = objectgenerator ] do %code.WriteLine(" set list(4) = ""FAILOVER""") do %code.WriteLine(" set list(5) = """"") do %code.WriteLine(" set response = ##class(%Library.Prompt).GetArray("_promptQuoted_",.value,.list,,,,"_defaultPromptFlag_")") + } elseif ((propertyDef) && (propertyDef.Name ="gitRemoteType")) { + do %code.WriteLine(" if (inst.gitRemoteURL '= """") { set value = inst.GetRemoteType(inst.gitRemoteURL)}") + } elseif ((propertyDef) && (propertyDef.Name = "OAuth")) { + do %code.WriteLine(" if (inst.gitRemoteType = ""HTTPS"") {") + do %code.WriteLine(" set value = 0") + do %code.WriteLine(" set response = ##class(%Library.Prompt).GetYesNo("_promptQuoted_",.value,,"_defaultPromptFlag) + do %code.WriteLine(" }") } else { do %code.WriteLine(" set response = ##class(%Library.Prompt).GetString("_promptQuoted_",.value,,,,"_defaultPromptFlag_")") } @@ -443,14 +469,58 @@ Method OnAfterConfigure() As %Boolean $$$ThrowOnError(workMgr.Queue("##class(SourceControl.Git.Utils).Init")) $$$ThrowOnError(workMgr.WaitForComplete()) } elseif (value = 2) { - set response = ##class(%Library.Prompt).GetString("Git remote URL (note: if authentication is required, use SSH, not HTTPS):",.remote,,,,defaultPromptFlag) + set remote = $select(..gitRemoteURL:..gitRemoteURL, 1:"") + set response = ##class(%Library.Prompt).GetString("Git remote URL:",.remote,,,,defaultPromptFlag) if (response '= $$$SuccessResponse) { quit } if (remote = "") { quit } - // using work queue manager ensures proper OS user context/file ownership + set ..gitRemoteURL = remote + set ..gitRemoteType = ..GetRemoteType(..gitRemoteURL) + do ..%Save() + + + if ((..gitRemoteType = "HTTPS") && ('..OAuth)) { + set value = 0 + set response = ##class(%Library.Prompt).GetYesNo("Do you want to use OAuth for your https remote",.value,,defaultPromptFlag) + if (response '= $$$SuccessResponse) { + quit + } + set ..OAuth = value + do ..%Save() + } + + + if ((..gitRemoteType = "HTTPS") && ..OAuth) { + set remote = ##class(SourceControl.Git.OAuth2).FixRemoteURL(remote) + Write !, "Please navigate to the Embedded Git UI on your browser, and press ""Authenticate"" in the bottom left corner." + Write !, "Once that process is complete, return here to verify cloning was successful" + Write !, "*Note: You must log in to the Management Portal as the current user" + + // poll attempt count + set try = 0 + // poll every `SLEEPTIME` seconds + set SLEEPTIME = 5 + // stop polling after `TIMEOUT` seconds + set TIMEOUT = 300 + While try*SLEEPTIME < TIMEOUT { + do ##class(SourceControl.Git.Util.CredentialManager).GetToken($username, .err, .code) + if code '= 1 { + Write "Unable to query credential manager" + // something went wrong, return from method + return + } + + if err = "" { + // token was saved successfully, exit loop + quit + } + } + } + + // using work queue manager ensures proper OS user context/file ownership set workMgr = $System.WorkMgr.%New("") $$$ThrowOnError(workMgr.Queue("##class(SourceControl.Git.Utils).Clone",remote)) $$$ThrowOnError(workMgr.WaitForComplete()) @@ -522,5 +592,9 @@ Method SaveDefaults() As %Boolean return ##class(%zpkg.isc.sc.git.Defaults).SetDefaultSettings(defaults) } +ClassMethod GetRemoteType(remoteURL As %String) As %String +{ + return $select(remoteURL [ "https": "HTTPS",1:"SSH") } +} diff --git a/cls/SourceControl/Git/Util/CredentialManager.cls b/cls/SourceControl/Git/Util/CredentialManager.cls new file mode 100644 index 00000000..c50e03e3 --- /dev/null +++ b/cls/SourceControl/Git/Util/CredentialManager.cls @@ -0,0 +1,237 @@ +Class SourceControl.Git.Util.CredentialManager Extends %RegisteredObject +{ + +/// Description +Property pvtStore [ Internal, Private ]; + +ClassMethod Test() [ Private ] +{ + Do ##class(SourceControl.Git.Util.CredentialManager).Stop() + + set username = "testUser" + w "Getting token for user """_username_""", expect error", ! + set token = ..GetToken(username, .err, .code) + zw token, err, code + + set token = "testToken" + w !, "Setting token """_token_""" for user """_username_"""", ! + do ..SetToken(username, token, .err, .code) + zw err, code + + w !, "Getting token for user "_username_", expect to get token """_token_"""", ! + set token = ..GetToken(username, .err, .code) + zw token, err, code +} + +ClassMethod Test2() [ Private ] +{ + set username = "testUser" + set token = ..GetToken(username, .err, .code) + zw token, err, code +} + +/// Creates the `..GetEventName()` named event +/// Waits on signals and services request +Method Run() [ Private ] +{ + do ##class(%SYSTEM.Event).Create(..GetEventName()) + + set i%pvtStore = ##class(PrivateMemoryStore).%New() + set code = 0 + while (code '= -1) { + try { + set code = ..Wait(.msgType, .msgContent) + if (code = 1) { + do ..HandleMessage(msgType, msgContent) + } + } catch err { + do err.Log() + } + } +} + +/// GetToken is used to retreive the access token for a particular git user +/// gitUsername (optional) default: `""` -- username for which token is to be retreived +/// error -- pass by reference -- returns error: if error '= "" we got an error from the daemon process +/// code -- pass by reference -- returns a code (refer to $SYSTEM.Event.Wait() for descriptions of possible values) +/// Returns fetched token +ClassMethod GetToken(gitUsername As %String = "", Output error As %String, Output code As %String) As %String +{ + set $lb(token, error) = ..Signal("GET", $lb($JOB, gitUsername), .code) + return token +} + +/// SetToken is used to set the access token for a particular git user +/// gitUsername (optional) default: `""` -- username for which token is to be set +/// error -- pass by reference -- returns error: if (error '= "") we got an error from the daemon process +/// code -- pass by reference -- returns a code (refer to $SYSTEM.Event.Wait() for descriptions of possible values) +ClassMethod SetToken(gitUsername As %String = "", gitToken As %String, Output error As %String, Output code) +{ + set $lb(, error) = ..Signal("SET", $lb($JOB, gitUsername, gitToken), .code) +} + +ClassMethod SendResponse(toPID As %Integer, message As %String, error As %String) [ Private ] +{ + if $System.Event.Signal(toPID, $lb(message, error)) '= 1 { + #; do ..LogForDaemon("Unable to send message: """_message_""" to: "_toPID) + } +} + +/// SetKeyPair is used to set the value for a key-value pair for a particular git user +/// gitUsername default: `""` -- username for which value is to be set +/// error -- pass by reference -- returns error: if (error '= "") we got an error from the daemon process +/// code -- pass by reference -- returns a code (refer to $SYSTEM.Event.Wait() for descriptions of possible values) +ClassMethod SetKeyPair(gitUsername As %String = "", key As %String, value As %String, Output error As %String, Output code) +{ + if (gitUsername = "") { + do ..SendResponse($JOB, "", "provide username") + q + } + set $lb(,error) = ..Signal("SET",$lb($JOB,gitUsername_"-"_key,value), .code) +} + +/// GetKeyPair is used to retreive the value for a key for a particular git user +/// gitUsername default: `""` -- username for which the value is to be retreived +/// error -- pass by reference -- returns error: if error '= "" we got an error from the daemon process +/// code -- pass by reference -- returns a code (refer to $SYSTEM.Event.Wait() for descriptions of possible values) +/// Returns fetched value +ClassMethod GetKeyPair(gitUsername As %String = "", key As %String, Output error As %String, Output code) As %String +{ + set $lb(value, error) = ..Signal("GET", $lb($JOB, gitUsername_"-"_key), .code) + return value +} + +Method HandleMessage(msgType As %String, msgContent As %String) [ Private ] +{ + try { + // make sure the message is appropriately formatted + set $lb(senderPID, gitUsername, gitToken) = msgContent + } catch err { + do err.Log() + quit + } + + if '$data(senderPID) { + #; do ..LogForDaemon("No source PID provided") + quit + } + + if '$data(gitUsername) { + do ..SendResponse(senderPID, "", "provide username") + quit + } + + set irisUsername = ##class(%SYS.ProcessQuery).%OpenId(senderPID).UserName + // key that the token would be mapped from + set key = $lb(irisUsername, gitUsername) + if msgType = "GET" { + if i%pvtStore.KeyExists(key) { + do ..SendResponse(senderPID, i%pvtStore.Retrieve(key), "") + } else { + do ..SendResponse(senderPID, "", "key does not exist") + } + } elseif msgType = "SET" { + if '$data(gitToken) { + do ..SendResponse(senderPID, "", "provide git token") + quit + } + do i%pvtStore.Store(key, gitToken) + do ..SendResponse(senderPID, gitToken, "") + } +} + +ClassMethod Signal(msgType As %String, msgContent As %String, Output responseCode) As %String [ Private ] +{ + // Make sure the daemon is running + do ..Start() + + // Clear any pending messages for this process' resource + do $System.Event.Clear($Job) + + // Signal the daemon + do ##class(%SYSTEM.Event).Signal(..GetEventName(),$ListBuild(msgType,msgContent)) + set $listbuild(responseCode,msg) = $System.Event.WaitMsg("",5) + return msg +} + +Method Wait(Output msgType As %String, Output msgContent As %String) As %Integer +{ + set (msg,msgType,msgContent) = "" + set $listbuild(code,msg) = ##class(%SYSTEM.Event).WaitMsg(..GetEventName(),1) + if $listvalid(msg) { + set $listbuild(msgType,msgContent) = msg + } + return code +} + +ClassMethod GetEventName() As %String [ Private ] +{ + return $Name(^isc.git.sc("Daemon")) //^"_$classname() +} + +ClassMethod Start() [ Private ] +{ + if ..CheckStatus() { + quit + } + job ..StartInternal():(:::1):5 + if ('$test) { + $$$ThrowStatus($$$ERROR($$$GeneralError,"Daemon process failed to start")) + } + while '$System.Event.Defined(..GetEventName()) { + hang 1 + if $increment(wait) > 5 { + // this is a no-no situation, right? + // we would never want to return from Start without starting + quit + } + } +} + +ClassMethod StartInternal() +{ + try { + set lock = $System.AutoLock.Lock(..GetEventName(), , 2) + set daemon = ..%New() + do daemon.Run() + } catch err { + #; do LogForDaemon(err.DisplayString()) + } +} + +ClassMethod StopMemoryStore() +{ + do ..Stop() +} + +ClassMethod Stop() [ Private ] +{ + do ##class(%SYSTEM.Event).Delete(..GetEventName()) + set pid = ^$LOCK(..GetEventName(), "OWNER") + if (pid > 0) { + do $System.Process.Terminate(pid) + } +} + +ClassMethod Restart() [ Private ] +{ + do ..Stop() + do ..Start() +} + +ClassMethod CheckStatus() As %Boolean [ Private ] +{ + return ($data(^$LOCK(..GetEventName())) = 10) +} + +/// This callback method is invoked by the %Close method to +/// provide notification that the current object is being closed. +/// +///

The return value of this method is ignored. +Method %OnClose() As %Status [ Private, ServerOnly = 1 ] +{ + do ##class(%SYSTEM.Event).Delete(..GetEventName()) + return $$$OK +} + +} diff --git a/cls/SourceControl/Git/Util/PrivateMemoryStore.cls b/cls/SourceControl/Git/Util/PrivateMemoryStore.cls new file mode 100644 index 00000000..1c7e0654 --- /dev/null +++ b/cls/SourceControl/Git/Util/PrivateMemoryStore.cls @@ -0,0 +1,192 @@ +/// Key-value store where values are stored in private memory (for high security). +Class SourceControl.Git.Util.PrivateMemoryStore Extends %RegisteredObject +{ + +Property buffer [ Internal, Private ]; + +Property map [ Internal, MultiDimensional, Private ]; + +Property offset [ InitialExpression = 0, Internal, Private ]; + +Property size [ Internal, Private ]; + +Parameter defaultSize = 128; + +Method %OnNew(size As %Integer) As %Status [ Private, ServerOnly = 1 ] +{ + if $DATA(size) && $ISVALIDNUM(size) && (size >= 0) { + set i%size = size + } else { + set i%size = ..#defaultSize + } + + set i%buffer = $zu(106,1,i%size) + return $$$OK +} + +/// Store `key`: `value` to the map +/// throws an exception if key = "" +Method Store(key As %String, value As %String) +{ + if key = "" { + throw ##class(%Exception.General).%New("INVALID_KEY_EXCEPTION","999",, "Invalid key for private memory store `""""`") + } + set length = $length(value) + // this will clear it if it exists + do ..Clear(key) + set requiredSize = length + i%offset + if (requiredSize > i%size) { + // TODO: there is definitely a better way to find the appropriate next size + // using log_2() but won't do that right now + + if i%size=0 { + set newSize = i%defaultSize + } else { + set newSize = i%size*2 + } + + while requiredSize > newSize { + set newSize = newSize*2 + } + set newBuffer = $zu(106,1,newSize) + + // move values from buffer to newBuffer + do ..compactBuffer(newBuffer, .newMap, .newOffset) + + // clear current buffer and deallocate + do ..deallocateBuffer() + + // set to new values + set i%buffer = newBuffer + set i%size = newSize + set i%offset = newOffset + merge i%map = newMap + } + // add mapping for the key + set i%map(key) = $lb(i%offset,length) + set i%offset = ..insertIntoMemoryStore(value, i%buffer, i%offset) +} + +/// Retreives the value associated with `key` +/// Returns `""` if key does not exist +Method Retrieve(key As %String) As %RawString +{ + return:('..KeyExists(key)) "" + + set $listbuild(offset,length) = i%map(key) + return $view(i%buffer+offset,-3,-length) +} + +/// Deletes the key and its associated value from the map +/// Returns silently if key does not exist +Method Clear(key As %String) +{ + quit:('..KeyExists(key)) + + kill i%map(key) + + do ..compactBuffer(i%buffer, .newMap, .newOffset) + // update the map and offset + kill i%map + merge i%map = newMap + set i%offset = newOffset +} + +Method %OnClose() As %Status [ Private, ServerOnly = 1 ] +{ + do ..deallocateBuffer() +} + +/// Returns true if `key` exists in the map +/// Returns false otherwise +Method KeyExists(key As %String) As %Boolean +{ + if key = "" { + return 0 + } + return $Get(i%map(key)) '= "" +} + +// PRIVATE METHODS ====> + +// Writes to Buffer and returns new offset + +Method insertIntoMemoryStore(value, buffer, offset As %Integer) As %Integer [ Private ] +{ + set length = $length(value) + view buffer+offset:-3:-length:value + return offset + length +} + +Method clearBuffer() [ Private ] +{ + // nulls out buffer + FOR i = 1:1:i%size { + view i%buffer+i:-3:-1:0 + } +} + +// iterate through ..map and move data from ..buffer to buffer + +Method compactBuffer(buffer, Output newMap, Output newOffset) [ Private ] +{ + // pointer to the next place to insert into the buffer + set newOffset = 0 + kill newMap + + do ..getInverseMap(.inverseMap) + // iterate through the offsets in ascending order + set curOffset = "" + for { + set curOffset = $order(inverseMap(curOffset)) + quit:(curOffset = "") + + set key = inverseMap(curOffset) + set value = ..Retrieve(key) + set newMap(key) = $lb(newOffset, $length(value)) + set newOffset = ..insertIntoMemoryStore(value, buffer, newOffset) + } +} + +Method deallocateBuffer() [ Private ] +{ + do ..clearBuffer() + set i%size = 0 + kill i%map + do $zu(106,0,i%buffer) +} + +// using this method to iterate by sorted offset + +// inverseMap is of array type + +Method getInverseMap(Output inverseMap) [ Private ] +{ + kill inverseMap + set iterKey = "" + for { + set iterKey = $order(i%map(iterKey)) + quit:(iterKey = "") + + set list = i%map(iterKey) + set $listbuild(offset,) = list + + set inverseMap(offset) = iterKey + } +} + +// util method to print ..map + +Method printMap() +{ + set iterKey = "" + w ! + for { + set iterKey = $order(i%map(iterKey)) + quit:(iterKey = "") + + w iterKey, ": ", $LISTTOSTRING(i%map(iterKey)), ! + } +} + +} diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index 924da7dd..0f392ee4 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -82,6 +82,21 @@ ClassMethod DecomposeProdAllowIDE() As %Boolean [ CodeMode = expression ] $Get(@..#Storage@("settings","decomposeProdAllowIDE"), 1) } +ClassMethod UsingOAuth() As %Boolean [ CodeMode = expression ] +{ +$Get(@..#Storage@("settings","OAuth"), 0) +} + +ClassMethod GitRemoteType() As %String +{ + return ##class(SourceControl.Git.Settings).GetRemoteType(..GitRemoteURL()) +} + +ClassMethod GitRemoteURL() As %String [ CodeMode = expression ] +{ +$Get(@..#Storage@("settings","gitRemoteURL"), 0) +} + ClassMethod FavoriteNamespaces() As %String { set favNamespaces = [] @@ -272,6 +287,9 @@ ClassMethod UserAction(InternalName As %String, MenuName As %String, ByRef Targe } elseif (menuItemName = "GitWebUI") { set Action = 2 + externalBrowser set Target = urlPrefix _ "/isc/studio/usertemplates/gitsourcecontrol/webuidriver.csp/"_$namespace_"/"_$zconvert(InternalName,"O","URL")_"?"_urlPostfix + } elseif (menuItemName = "Authenticate") { + set Action = 2 + externalBrowser + set Target = urlPrefix _ "/isc/studio/usertemplates/gitsourcecontrol/oauth2.csp?"_urlPostfix } elseif (menuItemName = "Export") || (menuItemName = "ExportForce") { write !, "==export start==",! set ec = ..ExportAll($case(menuItemName="ExportForce",1:$$$Force,:0)) @@ -1984,6 +2002,32 @@ ClassMethod RunGitCommandWithInput(command As %String, inFile As %String = "", O set gitCommand = $extract(..GitBinPath(),2,*-1) set baseArgs = "/STDOUT="_$$$QUOTE(outLog)_" /STDERR="_$$$QUOTE(errLog)_$case(inFile, "":"", :" /STDIN="_$$$QUOTE(inFile)) + // Use OAuth Authentication if needed (TODO: Should this be done always, or only for certain commands?) + if ((..UsingOAuth())) { + set token = ##class(SourceControl.Git.OAuth2).GetToken() + if (token '= "") { + if ($$$isWINDOWS) { + set askpassFile = "C:\Windows\Temp\askpass.bat" + if ('##class(%File).Exists(askpassFile)) { + open askpassFile:"NW":0 + use askpassFile Write "@echo off",!,"echo %GIT_TOKEN%",! + close askpassFile + } + } else { + set askpassFile = "/tmp/askpass.sh" + if ('##class(%File).Exists(askpassFile)) { + open askpassFile:"NW":0 + use askpassFile Write "#!/bin/bash",!,"echo $GIT_TOKEN",! + close askpassFile + // Make file executable + Do $zf(-1, "chmod +x "_askpassFile) + } + } + set env("GIT_TOKEN") = token + set env("GIT_ASKPASS") = askpassFile + set env("GIT_TERMINAL_PROMPT") = 0 + } + } try { // Inject instance manager directory as global git config home directory // On Linux, this avoids trying to use /root/.config/git/attributes for global git config @@ -1991,10 +2035,12 @@ ClassMethod RunGitCommandWithInput(command As %String, inFile As %String = "", O set returnCode = $zf(-100,"/ENV=env... "_baseArgs,gitCommand,newArgs...) } catch e { if $$$isWINDOWS { - set returnCode = $zf(-100,baseArgs,gitCommand,newArgs...) + k env("XDG_CONFIG_HOME") + set returnCode = $zf(-100,"/ENV=env... "_baseArgs,gitCommand,newArgs...) } else { + k env("XDG_CONFIG_HOME") // If can't inject XDG_CONFIG_HOME (older IRIS version), need /SHELL on Linux to avoid permissions errors trying to use root's config - set returnCode = $zf(-100,"/SHELL "_baseArgs,gitCommand,newArgs...) + set returnCode = $zf(-100,"/SHELL /ENV=env... "_baseArgs,gitCommand,newArgs...) } } @@ -3026,6 +3072,7 @@ ClassMethod BaselineExport(pCommitMessage = "", pPushToRemote = "") As %Status /// Returns the url for the "origin" remote repository ClassMethod GetConfiguredRemote(Output remoteExists As %Boolean = 0, Output sc As %Status = {$$$OK}) As %String { + set url = "" set exitCode = ..RunGitCommand("remote",.err,.out,"get-url","origin") if (exitCode = 0) { set remoteExists = 1 @@ -3053,14 +3100,17 @@ ClassMethod SetConfiguredRemote(url) As %String { do ..GetConfiguredRemote(.remoteExists) set returnCode = $select( - remoteExists&&(url=""): ##class(SourceControl.Git.Utils).RunGitCommandWithInput("remote",,.errStream,.outStream,"remove","origin"), - remoteExists&&(url'=""): ##class(SourceControl.Git.Utils).RunGitCommandWithInput("remote",,.errStream,.outStream,"set-url","origin",url), - 'remoteExists&&(url'=""): ##class(SourceControl.Git.Utils).RunGitCommandWithInput("remote",,.errStream,.outStream,"add","origin",url), - 1: 0) + (remoteExists&&(url="")): ##class(SourceControl.Git.Utils).RunGitCommandWithInput("remote",,.errStream,.outStream,"remove","origin"), + (remoteExists&&(url'="")): ##class(SourceControl.Git.Utils).RunGitCommandWithInput("remote",,.errStream,.outStream,"set-url","origin",url), + (('remoteExists)&&(url'="")): ##class(SourceControl.Git.Utils).RunGitCommandWithInput("remote",,.errStream,.outStream,"add","origin",url), + 1: -1) + if (returnCode = -1) { + quit "" + } if (returnCode '= 0) { $$$ThrowStatus($$$ERROR($$$GeneralError,"git reported failure")) } - set output = outStream.ReadLine(outStream.Size) + set output = outStream.Read() quit output } @@ -3203,6 +3253,31 @@ ClassMethod GitUnstage(Output output As %Library.DynamicObject) As %Status return $$$OK } +ClassMethod WriteLineToFile(filePath As %String, line As %String) +{ + Set file=##class(%File).%New(filePath) + Do file.Open("WSN") + Do file.WriteLine(line) +} + +ClassMethod Authenticated() As %Boolean +{ + if (##class(SourceControl.Git.Utils).UsingOAuth()) { + try { + // Run a git command that requires access to the remote + // if does not work, then we are unauthenticated + set returncode = ##class(SourceControl.Git.Utils).RunGitCommandWithInput("ls-remote","",.errStream,.outStream,) + do errStream.Rewind() + if (errStream.ReadLine()) '= "" { + return 0 + } + } catch e { + return 0 + } + } + return 1 +} + ClassMethod IsSchemaStandard(pName As %String = "") As %Boolean [ Internal ] { Set parts = $Length(pName,".") diff --git a/cls/SourceControl/Git/WebUIDriver.cls b/cls/SourceControl/Git/WebUIDriver.cls index a7d808cc..e462d1c8 100644 --- a/cls/SourceControl/Git/WebUIDriver.cls +++ b/cls/SourceControl/Git/WebUIDriver.cls @@ -18,6 +18,8 @@ ClassMethod HandleRequest(pagePath As %String, InternalName As %String = "", Out set responseJSON = ..Uncommitted() } elseif $extract(pagePath,6,*) = "settings" { set responseJSON = ..GetSettingsURL(%request) + } elseif $extract(pagePath, 6, *) = "oauth" { + set responseJSON = ..GetOAuthURL(%request) } elseif $extract(pagePath, 6, *) = "get-package-version"{ set responseJSON = ..GetPackageVersion() } elseif $extract(pagePath, 6, *) = "git-version" { @@ -436,6 +438,17 @@ ClassMethod GetSettingsURL(%request As %CSP.Request) As %SystemBase quit {"url": (settingsURL)} } +ClassMethod GetOAuthURL(%request As %CSP.Request) As %SystemBase +{ + set oauthURL = "" + if ('##class(SourceControl.Git.Utils).Authenticated()) { + set oauthURL = "/isc/studio/usertemplates/gitsourcecontrol/oauth2.csp?CSPSHARE=1&Namespace="_$namespace_"&Username="_$username + set oauthURL = ..GetURLPrefix(%request, oauthURL) + } + set ^mtempet = oauthURL + quit {"url": (oauthURL)} +} + ClassMethod GetPackageVersion() As %Library.DynamicObject { set version = ##class(SourceControl.Git.Utils).GetPackageVersion() diff --git a/cls/_zpkg/isc/sc/git/SSLConfig.cls b/cls/_zpkg/isc/sc/git/SSLConfig.cls new file mode 100644 index 00000000..07e81d22 --- /dev/null +++ b/cls/_zpkg/isc/sc/git/SSLConfig.cls @@ -0,0 +1,41 @@ +Class %zpkg.isc.sc.git.SSLConfig +{ + + ClassMethod CreateSSLConfigIfNonExistent(name As %String) { + try { + do ..CheckSSLConfig(name) + } catch e { + return e.AsStatus() + } + return $$$OK + } + + ClassMethod CheckSSLConfig(name As %String) [ Private, NotInheritable ] { + $$$AddAllRoleTemporary + new $namespace + set $namespace = "%SYS" + + do ##class(Security.SSLConfigs).Get(name, .p) + if $data(p) quit + + set p("CipherList")="ALL:!aNULL:!eNULL:!EXP:!SSLv2" + set p("CAFile")="" + set p("CAPath")="" + set p("CRLFile")="" + set p("CertificateFile")="" + set p("CipherList")="ALL:!aNULL:!eNULL:!EXP:!SSLv2" + set p("Description")="" + set p("Enabled")=1 + set p("PrivateKeyFile")="" + set p("PrivateKeyPassword")="" + set p("PrivateKeyType")=2 + set p("Protocols")=24 + set p("SNIName")="" + set p("Type")=0 + set p("VerifyDepth")=9 + set p("VerifyPeer")=0 + + do ##class(Security.SSLConfigs).Create(name, .p) + } + +} \ No newline at end of file diff --git a/csp/gitprojectsettings.csp b/csp/gitprojectsettings.csp index cb92fcb3..6a1e5bc2 100644 --- a/csp/gitprojectsettings.csp +++ b/csp/gitprojectsettings.csp @@ -120,6 +120,12 @@ body { } + if ((newRemote [ "https") && ($Get(%request.Data("OAuth"), 1) = 1) ) { + set settings.OAuth = 1 + } else { + set settings.OAuth = 0 + } + set settings.compileOnImport = ($Get(%request.Data("compileOnImport", 1)) = 1) set settings.decomposeProductions = ($Get(%request.Data("decomposeProductions", 1)) = 1) set settings.decomposeProdAllowIDE = ($Get(%request.Data("decomposeProdAllowIDE", 1)) = 1) @@ -167,6 +173,7 @@ body { if ($get(%request.Data("proxySubmitButton",1)) = "saveDefaults") { do settings.SaveDefaults() } + do settings.%Save() } set err = "" try { @@ -197,9 +204,29 @@ body { if (remote'="") && (##class(SourceControl.Git.Utils).RunGitCommandWithInput("ls-remote",,.errStream,,"origin")'=0) { set remoteConnectionError = errStream.Read() } -} catch err { - do err.Log() - &html<

An error occurred and has been logged to the application error log.
> + } catch err { + do err.Log() + &html<
An error occurred and has been logged to the application error log.
> + if ($get(%request.Data("proxySubmitButton",1)) = "saveDefaults") { + do settings.SaveDefaults() + } + } + set err = "" + try { + set buffer = ##class(SourceControl.Git.Util.Buffer).%New() + do buffer.BeginCaptureOutput() + $$$ThrowOnError(settings.SaveWithSourceControl()) + do buffer.EndCaptureOutput(.out) + if (out '= "") { + &html<
+
#(..EscapeHTML(out))#
+
> + } + } catch err { + kill buffer + do err.Log() + &html<
An error occurred and has been logged to the application error log.
> + } }
@@ -441,6 +468,16 @@ body { Connection successful
+ + +
@@ -757,6 +794,19 @@ var submitForm = function(e) { form.submit(); } +const remoteURL = document.getElementById('remoteRepo'); +if ((remoteURL.value.toLowerCase().includes('https'))) { + document.getElementById('remoteOAuth').style.display = 'flex'; +} +remoteURL.addEventListener('change', function () { + console.log(remoteURL.value) + if ((remoteURL.value.toLowerCase().includes('https'))) { + document.getElementById('remoteOAuth').style.display = 'flex'; + } else { + document.getElementById('remoteOAuth').style.display = 'none'; + } +}); + document.getElementById('saveDefaults').addEventListener('click',submitForm,false); function init() { diff --git a/csp/oauth2.csp b/csp/oauth2.csp new file mode 100644 index 00000000..01750cf2 --- /dev/null +++ b/csp/oauth2.csp @@ -0,0 +1,323 @@ + + + + + + +HTTPS OAuth Configuration + + + + + + + set failed = 0 + set authenticated = 0 + if $Data(%request.Data("state",1),state)#2 { + // Redirected here from github + // switch to the namespace that the extension is installed to + set namespace = $Piece(state,"_",1) + new $Namespace + set $Namespace = namespace + + + set config = ##class(SourceControl.Git.OAuth2.Config).GetConfig($username) + if (config.state '= state){ + set failed =1 + set failuremessage = "Invalid state" + quit 1 + } + + if '$Data(%request.Data("code",1),code)#2 { + set failed =1 + set failuremessage = "Invalid request parameters" + quit 1 + } + + set verifier = config.verifier + set result = config.Exchange(code, verifier, .sc) + if sc '= $$$OK { + do $SYSTEM.Status.DisplayError(sc) + set failed = 1 + set failuremessage = "Unable to retrieve access token" + } else { + do ##class(SourceControl.Git.Util.CredentialManager).SetToken($username,result, .err, .code) + if (code '= 1) || (err '= "") { + set failed = 1 + set failuremessage = "Unable to save credentials" + } else { + if (##class(SourceControl.Git.OAuth2).GetToken() '= "") { + set authenticated = 1 + } else { + set failed = 1 + set failuremessage = "Something went wrong" + } + } + } + } + set namespace = $NAMESPACE + set username = $USERNAME + set config = ##class(SourceControl.Git.OAuth2.Config).GetConfig(username) + + if (config = "") { + set authURL = "" + set tokenURL = "" + set remote = ##class(SourceControl.Git.Utils).GetConfiguredRemote() + set urls = ##class(SourceControl.Git.OAuth2).GetURLsFromRemote(remote,.authURL,.tokenURL) + } + set authCodeURL = "" + set ready = "" + /// After submit + if (%request.Method="POST") && $Data(%request.Data("oauthsettings",1)) { + set ready = 1 + set clientID = $Get(%request.Data("clientID",1)) + set clientSecret = $Get(%request.Data("clientSecret",1)) + set authURL = $Get(%request.Data("authURL",1)) + set tokenURL = $Get(%request.Data("tokenURL",1)) + set redirect = $piece($Get(%request.Data("redirectURL",1)),"?",1) + set usingDirectToken = $Get(%request.Data("usingDirectToken"), 1) + set directToken = $Get(%request.Data("directToken", 1)) + + /// This make sure private memory store works + do ##class(SourceControl.Git.Util.CredentialManager).StopMemoryStore() + + if config = "" { + set config = ##class(SourceControl.Git.OAuth2.Config).%New(username, clientID, clientSecret,authURL,tokenURL,redirect) + } else { + set config.ClientID = clientID + set config.ClientSecret = clientSecret + set config.Endpoint.AuthURL = authURL + set config.Endpoint.TokenURL = tokenURL + set config.RedirectURL = redirect + set config.directToken = $select(usingDirectToken = 1: 1, 1: 0) + } + + do config.%Save() + if (usingDirectToken '= 1) { + set authCodeURL = ##class(SourceControl.Git.OAuth2).AuthCodeURL(config,namespace,.state,.verifier) + set config.state = state + set config.verifier = verifier + } else { + do ##class(SourceControl.Git.Util.CredentialManager).SetToken($username,directToken, .err, .code) + set ready = "" + set authenticated = 1 + } + do config.%Save() + } + +
+ +
+ × + Success! You have been authenticated. You may now close this page. +
+
+ +
+ × + Error! #(failuremessage)# +
+
+ +
+
+

+ To connect your GitHub or GitLab repository, you need to generate a Client ID and Client Secret. + Follow these steps: +

+ +

Once generated, enter your Client ID and Client Secret below:

+

The "Authorization callback URL" should be: text

+
+
+ +
+
+ + set directToken = $select(config:config.directToken, 1: 0) + if (directToken) { + &html<> + } else { + &html<> + } + + +
+
+
+ + +
+
+ +
+ + set clientID = $select(config:config.ClientID, 1: "") + + +
+
+ +
+ +
+ + set clientSecret = $select(config:config.ClientSecret, 1: "") + + +
+
+ +
+ +
+ + set authURL = $select(config:config.Endpoint.AuthURL,authURL'="":authURL, 1: "") + + +
+
+ +
+ +
+ + set tokenURL = $select(config:config.Endpoint.TokenURL, tokenURL'="":tokenURL, 1: "") + + +
+
+
+ +
+
+ + +
+
+ + +
+ + + + + + +
+ + + + + + + \ No newline at end of file diff --git a/docs/https.md b/docs/https.md new file mode 100644 index 00000000..2a97249f --- /dev/null +++ b/docs/https.md @@ -0,0 +1,18 @@ +## Setting up HTTPS + +We highly recommend that you use SSH to connect to your repositories. If this is not possible, then HTTPS is another option. + +First, add your remote repo in the settings page, or during the Configure step. (Note: do NOT provide a username in the url) + +After this, you have to authenticate using OAuth tokens. To do this, press "Authenticate" in the bottom left of the Embedded Git UI, or from the Source Control Menu. + +### Authentication + +If you have not already done so, create a new OAuth app in github or gitlab. The "Authorization callback URL" should be <your url>/isc/studio/usertemplates/gitsourcecontrol/oauth2.csp. + +Remember to save the ClientID and ClientSecret. Once this is finished, you can enter your information into the authentication page. +![Screenshot of authentication page](images/auth.png) + +Once all of the information is correct, you can press Save. This will redirect you to either gitlab or github in order to authorize your application. After this is done, you will be redirected back to the authentication page, and you should be good to go! + + diff --git a/docs/images/auth.png b/docs/images/auth.png new file mode 100644 index 00000000..e6f7e7ac Binary files /dev/null and b/docs/images/auth.png differ diff --git a/git-webui/release/share/git-webui/webui/css/git-webui.css b/git-webui/release/share/git-webui/webui/css/git-webui.css index 02e47be8..12e9c644 100644 --- a/git-webui/release/share/git-webui/webui/css/git-webui.css +++ b/git-webui/release/share/git-webui/webui/css/git-webui.css @@ -241,6 +241,22 @@ body { width: 16.4em; background-color: #333333; } +#sidebar #sidebar-content #sidebar-oauth a { + color: white; +} +#sidebar #sidebar-content #sidebar-oauth h4 { + padding: 0px; + margin-bottom: 10px; +} +#sidebar #sidebar-content #sidebar-oauth h4:before { + content: url(../img/oauth.svg); +} +#sidebar #sidebar-content #sidebar-oauth { + position: absolute; + bottom: 120px; + width: 16.4em; + background-color: #333333; +} #sidebar #sidebar-content #sidebar-context h4 { padding: 0px; margin-bottom: 10px; diff --git a/git-webui/release/share/git-webui/webui/img/oauth.svg b/git-webui/release/share/git-webui/webui/img/oauth.svg new file mode 100644 index 00000000..b2c4fa95 --- /dev/null +++ b/git-webui/release/share/git-webui/webui/img/oauth.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/git-webui/release/share/git-webui/webui/js/git-webui.js b/git-webui/release/share/git-webui/webui/js/git-webui.js index ee5a08c4..f1d8d093 100644 --- a/git-webui/release/share/git-webui/webui/js/git-webui.js +++ b/git-webui/release/share/git-webui/webui/js/git-webui.js @@ -938,6 +938,10 @@ webui.SideBarView = function(mainView, noEventHandlers) { window.location.href = webui.settingsURL; } + self.goToOAuth = function() { + window.location.href = webui.oauthURL; + } + self.goToHomePage = function() { window.location.href = webui.homeURL; } @@ -993,6 +997,12 @@ webui.SideBarView = function(mainView, noEventHandlers) { self.mainView = mainView; self.currentContext = self.getCurrentContext(); + + var oauthHTML = ''; + if (webui.oauthURL != "") { + oauthHTML = '' + } + self.element = $( '