From e5642cc1b77ffccbda158e68ff5beee401017b0a Mon Sep 17 00:00:00 2001 From: Rutvik Saptarshi Date: Fri, 29 Sep 2023 11:05:36 -0400 Subject: [PATCH 01/27] finished implementation for private-memory-store, added .gitignore to cls\SourceControl\Git" --- cls/SourceControl/Git/.gitignore | 2 + .../Git/Util/PrivateMemoryStore.cls | 189 ++++++++++++++++++ 2 files changed, 191 insertions(+) create mode 100644 cls/SourceControl/Git/.gitignore create mode 100644 cls/SourceControl/Git/Util/PrivateMemoryStore.cls diff --git a/cls/SourceControl/Git/.gitignore b/cls/SourceControl/Git/.gitignore new file mode 100644 index 00000000..0401a6cc --- /dev/null +++ b/cls/SourceControl/Git/.gitignore @@ -0,0 +1,2 @@ +# using TestClass.cls for objecscript functionality exploration / convince myself that things work the way I expect them to +TestClass.cls \ No newline at end of file diff --git a/cls/SourceControl/Git/Util/PrivateMemoryStore.cls b/cls/SourceControl/Git/Util/PrivateMemoryStore.cls new file mode 100644 index 00000000..23705561 --- /dev/null +++ b/cls/SourceControl/Git/Util/PrivateMemoryStore.cls @@ -0,0 +1,189 @@ +/// 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 ]; + +Property defaultSize [ InitialExpression = 128, Internal, Private ]; + +Method %OnNew(size As %Integer) As %Status [ Private, ServerOnly = 1 ] +{ + if size <= 0 { + set size = i%defaultSize + } + set i%size = size + set i%buffer = $zu(106,1,size) + quit $$$OK +} + +Method Store(key, value) +{ + set length = $length(value) + // this will clear it if it exists + do ..Clear(key) + + if (length + i%offset > i%size) { + // TODO: there is definitely a better way to find the appropriate next size + // using log but won't do that right now + if i%size=0 { + set newSize = i%defaultSize + } else { + set newSize = i%size*2 + } + do { + set newSize = newSize*2 + } while (length + i%offset > newSize) + 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 + set i%map(key) = $lb(i%offset,length) + set i%offset = ..insertIntoMemoryStore(value, i%buffer, i%offset) +} + +Method Retrieve(key) As %RawString +{ + quit:('..KeyExists(key)) "" + + set $listbuild(offset,length) = i%map(key) + return $view(i%buffer+offset,-3,-length) +} + +Method Clear(key) +{ + 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() +} + +Method KeyExists(key) As %Boolean +{ + return '($Get(i%map(key)) = "") +} + +ClassMethod Test() +{ + set inst = ..%New(-2) + do inst.Store("foo","bar") + write !,"foo: ",inst.Retrieve("foo") + do inst.Store("foo2","Hello World!") + write !,"foo: ",inst.Retrieve("foo") + write !,"foo2: ",inst.Retrieve("foo2") + zw inst + break + do inst.Store("foo2","Bye World!") + do inst.Clear("foo") + do inst.Store("foo3","This is actually quite fun") + write !,"foo: ",inst.Retrieve("foo") + write !,"foo2: ",inst.Retrieve("foo2") + write !,"foo3: ",inst.Retrieve("foo3"), ! + zw inst +} + +// PRIVATE METHODS ====> + +Method insertIntoMemoryStore(value, buffer, offset) As %Integer [ Private ] +{ + set length = $length(value) + view buffer+offset:-3:-length:value + quit 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 ] +{ + w !, "deallocating ", i%buffer, " of size ", i%size, ! + do ..clearBuffer() + set i%size = 0 + kill i%map + do $zu(106,0,i%buffer) +} + +// using this method to iterate by sorted offset + +Method getInverseMap(Output inverseMap) [ Private ] +{ + 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)), ! + } +} + +} From 74a4facc8e36806e22111ad1e1f9bde8786d79cc Mon Sep 17 00:00:00 2001 From: Rutvik Saptarshi Date: Mon, 2 Oct 2023 15:05:34 -0400 Subject: [PATCH 02/27] added tests, updated constructor --- .gitignore | 5 +- cls/SourceControl/Git/.gitignore | 2 - .../Git/Util/PrivateMemoryStore.cls | 50 +++++++------------ module.xml | 1 + .../SourceControl/Git/PrivateMemoryStore.cls | 49 ++++++++++++++++++ .../Git/PrivateMemoryStoreTest.cls | 26 ++++++++++ 6 files changed, 98 insertions(+), 35 deletions(-) delete mode 100644 cls/SourceControl/Git/.gitignore create mode 100644 test/UnitTest/SourceControl/Git/PrivateMemoryStore.cls create mode 100644 test/UnitTest/SourceControl/Git/PrivateMemoryStoreTest.cls 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/cls/SourceControl/Git/.gitignore b/cls/SourceControl/Git/.gitignore deleted file mode 100644 index 0401a6cc..00000000 --- a/cls/SourceControl/Git/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -# using TestClass.cls for objecscript functionality exploration / convince myself that things work the way I expect them to -TestClass.cls \ No newline at end of file diff --git a/cls/SourceControl/Git/Util/PrivateMemoryStore.cls b/cls/SourceControl/Git/Util/PrivateMemoryStore.cls index 23705561..e0bee8b1 100644 --- a/cls/SourceControl/Git/Util/PrivateMemoryStore.cls +++ b/cls/SourceControl/Git/Util/PrivateMemoryStore.cls @@ -10,15 +10,17 @@ Property offset [ InitialExpression = 0, Internal, Private ]; Property size [ Internal, Private ]; -Property defaultSize [ InitialExpression = 128, Internal, Private ]; +Parameter defaultSize = 128; -Method %OnNew(size As %Integer) As %Status [ Private, ServerOnly = 1 ] +Method %OnNew(size) As %Status [ Private, ServerOnly = 1 ] { - if size <= 0 { - set size = i%defaultSize + if $DATA(size) && $ISVALIDNUM(size) && (size >= 0) { + set i%size = size + } else { + set i%size = ..#defaultSize } - set i%size = size - set i%buffer = $zu(106,1,size) + w !, "Size set to ", i%size, ! + set i%buffer = $zu(106,1,i%size) quit $$$OK } @@ -27,18 +29,20 @@ Method Store(key, value) set length = $length(value) // this will clear it if it exists do ..Clear(key) - - if (length + i%offset > i%size) { + set requiredSize = length + i%offset + if (requiredSize > i%size) { // TODO: there is definitely a better way to find the appropriate next size - // using log but won't do that right now + // using log_2() but won't do that right now + if i%size=0 { set newSize = i%defaultSize } else { set newSize = i%size*2 } - do { + + while requiredSize > newSize { set newSize = newSize*2 - } while (length + i%offset > newSize) + } set newBuffer = $zu(106,1,newSize) // move values from buffer to newBuffer @@ -53,7 +57,7 @@ Method Store(key, value) set i%offset = newOffset merge i%map = newMap } - // add mapping for the + // add mapping for the key set i%map(key) = $lb(i%offset,length) set i%offset = ..insertIntoMemoryStore(value, i%buffer, i%offset) } @@ -89,27 +93,10 @@ Method KeyExists(key) As %Boolean return '($Get(i%map(key)) = "") } -ClassMethod Test() -{ - set inst = ..%New(-2) - do inst.Store("foo","bar") - write !,"foo: ",inst.Retrieve("foo") - do inst.Store("foo2","Hello World!") - write !,"foo: ",inst.Retrieve("foo") - write !,"foo2: ",inst.Retrieve("foo2") - zw inst - break - do inst.Store("foo2","Bye World!") - do inst.Clear("foo") - do inst.Store("foo3","This is actually quite fun") - write !,"foo: ",inst.Retrieve("foo") - write !,"foo2: ",inst.Retrieve("foo2") - write !,"foo3: ",inst.Retrieve("foo3"), ! - zw inst -} - // PRIVATE METHODS ====> +// Writes to Buffer and returns new offset + Method insertIntoMemoryStore(value, buffer, offset) As %Integer [ Private ] { set length = $length(value) @@ -149,7 +136,6 @@ Method compactBuffer(buffer, Output newMap, Output newOffset) [ Private ] Method deallocateBuffer() [ Private ] { - w !, "deallocating ", i%buffer, " of size ", i%size, ! do ..clearBuffer() set i%size = 0 kill i%map diff --git a/module.xml b/module.xml index a05a04d8..68cdcbd6 100644 --- a/module.xml +++ b/module.xml @@ -15,6 +15,7 @@ + diff --git a/test/UnitTest/SourceControl/Git/PrivateMemoryStore.cls b/test/UnitTest/SourceControl/Git/PrivateMemoryStore.cls new file mode 100644 index 00000000..29cc14a2 --- /dev/null +++ b/test/UnitTest/SourceControl/Git/PrivateMemoryStore.cls @@ -0,0 +1,49 @@ +Class UnitTest.SourceControl.Git.PrivateMemoryStore Extends %UnitTest.TestCase +{ + +Method TestInsert() +{ + set pvtMemStore = ##class(SourceControl.Git.Util.PrivateMemoryStore).%New(16) + set keys = $lb("a", "b", "c") + set values = $lb("123456", "789101112", "131415161718") + set n = $ListLength(keys) + do ..InsertToMemoryStore(pvtMemStore, keys, values) + + for i=1:1:n { + Set key = $List(keys,i) + Set expectedValue = $List(values,i) + + do $$$AssertEquals(pvtMemStore.Retrieve(key), expectedValue) + } +} + +ClassMethod InsertToMemoryStore(store, keys, ByRef values) +{ + set n = $ListLength(keys) + for i=1:1:n { + Set key = $List(keys,i) + Set value = $List(values,i) + do store.Store(key, value) + } +} + +Method TestDelete() +{ + set pvtMemStore = ##class(SourceControl.Git.Util.PrivateMemoryStore).%New(16) + set keys = $lb("a", "b", "c") + set values = $lb("123456", "789101112", "131415161718") + do ..InsertToMemoryStore(pvtMemStore, keys, values) + set n = $ListLength(keys) + + for i=1:1:n-1 { + do pvtMemStore.Clear($List(keys,i)) + } + + for i=1:1:n-1 { + do $$$AssertEquals(pvtMemStore.KeyExists($List(keys,i)), 0) + } + + do $$$AssertEquals(pvtMemStore.Retrieve($List(keys,n)), $List(values,n)) +} + +} diff --git a/test/UnitTest/SourceControl/Git/PrivateMemoryStoreTest.cls b/test/UnitTest/SourceControl/Git/PrivateMemoryStoreTest.cls new file mode 100644 index 00000000..70b9488a --- /dev/null +++ b/test/UnitTest/SourceControl/Git/PrivateMemoryStoreTest.cls @@ -0,0 +1,26 @@ +Import SourceControl.Git.Util + +Class test.UnitTest.SourceControl.Git.Util.PrivateMemoryStoreTest Extends %UnitTest.TestCase +{ + +Method TestInsert() +{ + set pvtMemStore = ##class(PrivateMemoryStore).%New(16) + set keys = $lb("a", "b", "c") + set values = $lb("123456", "789101112", "131415161718") + set n = $ListLength(keys) + for i=1:1:n { + Set key = $List(keys,i) + Set value = $List(values,i) + + do pvtMemStore.Store(key, value) + } + for i=1:1:n { + Set key = $List(keys,i) + Set expectedValue = $List(values,i) + + do $$$AssertEquals(pvtMemStore.Retrieve(key), expectedValue) + } +} + +} From 767ac5caf74d4d1231af2f3f6d197d62a1f41ac1 Mon Sep 17 00:00:00 2001 From: Rutvik Saptarshi Date: Tue, 3 Oct 2023 13:26:40 -0400 Subject: [PATCH 03/27] started work on credential manager --- .../Git/Util/CredentialManager.cls | 143 ++++++++++++++++++ module.xml | 1 - .../Git/PrivateMemoryStoreTest.cls | 26 ---- 3 files changed, 143 insertions(+), 27 deletions(-) create mode 100644 cls/SourceControl/Git/Util/CredentialManager.cls delete mode 100644 test/UnitTest/SourceControl/Git/PrivateMemoryStoreTest.cls diff --git a/cls/SourceControl/Git/Util/CredentialManager.cls b/cls/SourceControl/Git/Util/CredentialManager.cls new file mode 100644 index 00000000..ca561bc5 --- /dev/null +++ b/cls/SourceControl/Git/Util/CredentialManager.cls @@ -0,0 +1,143 @@ +Class SourceControl.Git.Util.CredentialManager Extends %RegisteredObject +{ + +/// Description +Property pvtStore [ Internal, Private ]; + +ClassMethod Test() +{ + Do ##class(SourceControl.Git.Util.CredentialManager).Stop() + Set response = ##class(SourceControl.Git.Util.CredentialManager).Signal("getUsername",$job,.code) + Write ! zw response,code + Set response = ##class(SourceControl.Git.Util.CredentialManager).Signal("fakeType",$job,.code) + Write ! zw response,code +} + +Method Run() +{ + do ##class(%SYSTEM.Event).Create(..GetEventName()) + + set i%pvtStore = ##class(PrivateMemoryStore).%New() + set code = 0 + while (code '= -1) { + try { + set code = ..Wait(.msgType, .senderPID) + if (code = 1) { + do ..DaemonLogger("Code: "_code_" "_"Received "_msgType_" && "_ " "_senderPID) + do ..HandleMessage(msgType, senderPID) + } + } catch err { + do err.Log() + } + } +} + +ClassMethod HandleMessage(msgType, senderPID) +{ + set username = $System.Process.UserName(senderPID) +} + +ClassMethod Signal(msgType As %String, msgContent As %String, Output responseCode) As %String +{ + // Make sure the daemon is running + do ..Start() + + write "Event defined? ",$System.Event.Defined(..GetEventName()),! + + // 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) + quit msg +} + +Method Wait(Output msgType As %String, Output senderPID As %String) As %Integer +{ + set (msg,msgType,senderPID) = "" + set $listbuild(code,msg) = ##class(%SYSTEM.Event).WaitMsg(..GetEventName(),1) + if $listvalid(msg) { + set $listbuild(msgType,senderPID) = msg + } + quit code +} + +ClassMethod GetEventName() As %String +{ + return $Name(^isc.git.sc("Daemon")) //^"_$classname() +} + +ClassMethod Start() +{ + 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 err.Log() + } +} + +ClassMethod Stop() +{ + set deleted = ##class(%SYSTEM.Event).Delete(..GetEventName()) + w "deleted the event? ", deleted, ! + set pid = ^$LOCK(..GetEventName(), "OWNER") + if (pid > 0) { + do $System.Process.Terminate(pid) + } +} + +ClassMethod Restart() +{ + do ..Stop() + do ..Start() +} + +ClassMethod CheckStatus() As %Boolean +{ + 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()) + quit $$$OK +} + +Method DaemonLogger(msg) As %Status +{ + try { + do LOG^%ETN("!!!Daemon Message!!!"_msg) + set sc=$$$OK + } catch err { + set sc=err.AsStatus() + } + quit sc +} + +} diff --git a/module.xml b/module.xml index 68cdcbd6..a05a04d8 100644 --- a/module.xml +++ b/module.xml @@ -15,7 +15,6 @@ - diff --git a/test/UnitTest/SourceControl/Git/PrivateMemoryStoreTest.cls b/test/UnitTest/SourceControl/Git/PrivateMemoryStoreTest.cls deleted file mode 100644 index 70b9488a..00000000 --- a/test/UnitTest/SourceControl/Git/PrivateMemoryStoreTest.cls +++ /dev/null @@ -1,26 +0,0 @@ -Import SourceControl.Git.Util - -Class test.UnitTest.SourceControl.Git.Util.PrivateMemoryStoreTest Extends %UnitTest.TestCase -{ - -Method TestInsert() -{ - set pvtMemStore = ##class(PrivateMemoryStore).%New(16) - set keys = $lb("a", "b", "c") - set values = $lb("123456", "789101112", "131415161718") - set n = $ListLength(keys) - for i=1:1:n { - Set key = $List(keys,i) - Set value = $List(values,i) - - do pvtMemStore.Store(key, value) - } - for i=1:1:n { - Set key = $List(keys,i) - Set expectedValue = $List(values,i) - - do $$$AssertEquals(pvtMemStore.Retrieve(key), expectedValue) - } -} - -} From a7aefb0a4331f93d19c030bdc089b1ca924c384c Mon Sep 17 00:00:00 2001 From: Rutvik Saptarshi Date: Tue, 3 Oct 2023 16:10:49 -0400 Subject: [PATCH 04/27] done with basic implementation --- .../Git/Util/CredentialManager.cls | 97 +++++++++++++++---- 1 file changed, 78 insertions(+), 19 deletions(-) diff --git a/cls/SourceControl/Git/Util/CredentialManager.cls b/cls/SourceControl/Git/Util/CredentialManager.cls index ca561bc5..b4f781e6 100644 --- a/cls/SourceControl/Git/Util/CredentialManager.cls +++ b/cls/SourceControl/Git/Util/CredentialManager.cls @@ -7,10 +7,17 @@ Property pvtStore [ Internal, Private ]; ClassMethod Test() { Do ##class(SourceControl.Git.Util.CredentialManager).Stop() - Set response = ##class(SourceControl.Git.Util.CredentialManager).Signal("getUsername",$job,.code) - Write ! zw response,code - Set response = ##class(SourceControl.Git.Util.CredentialManager).Signal("fakeType",$job,.code) - Write ! zw response,code + + set username = "testUser" + set token = ..GetToken(username, .err, .code) + zw token, err, code + + set token = "testToken" + do ..SetToken(username, token, .err, .code) + zw err, code + + set token = ..GetToken(username, .err, .code) + zw token, err, code } Method Run() @@ -21,10 +28,10 @@ Method Run() set code = 0 while (code '= -1) { try { - set code = ..Wait(.msgType, .senderPID) + set code = ..Wait(.msgType, .msgContent) if (code = 1) { - do ..DaemonLogger("Code: "_code_" "_"Received "_msgType_" && "_ " "_senderPID) - do ..HandleMessage(msgType, senderPID) + do ..LogForDaemon("Code: "_code_" "_"Received "_msgType_" && "_ " "_msgContent) + do ..HandleMessage(msgType, msgContent) } } catch err { do err.Log() @@ -32,9 +39,59 @@ Method Run() } } -ClassMethod HandleMessage(msgType, senderPID) +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) + quit token +} + +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) { - set username = $System.Process.UserName(senderPID) + if $System.Event.Signal(toPID, $lb(message, error)) '= 1 { + do ..LogForDaemon("Unable to send message: """_message_""" to: "_toPID) + } +} + +Method HandleMessage(msgType As %String, msgContent) +{ + 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 + } + + if msgType = "GET" { + if i%pvtStore.KeyExists(gitUsername) { + do ..SendResponse(senderPID, i%pvtStore.Retrieve(gitUsername), "") + } else { + // what is a good way to indicate that key does not exist? + do ..SendResponse(senderPID, "", "key does not exist") + } + } elseif msgType = "SET" { + if '$data(gitToken) { + do ..SendResponse(senderPID, "", "provide git token") + quit + } + do i%pvtStore.Store(gitUsername, gitToken) + do ..SendResponse(senderPID, gitToken, "") + } } ClassMethod Signal(msgType As %String, msgContent As %String, Output responseCode) As %String @@ -42,8 +99,6 @@ ClassMethod Signal(msgType As %String, msgContent As %String, Output responseCod // Make sure the daemon is running do ..Start() - write "Event defined? ",$System.Event.Defined(..GetEventName()),! - // Clear any pending messages for this process' resource do $System.Event.Clear($Job) @@ -53,12 +108,12 @@ ClassMethod Signal(msgType As %String, msgContent As %String, Output responseCod quit msg } -Method Wait(Output msgType As %String, Output senderPID As %String) As %Integer +Method Wait(Output msgType As %String, Output msgContent As %String) As %Integer { - set (msg,msgType,senderPID) = "" + set (msg,msgType,msgContent) = "" set $listbuild(code,msg) = ##class(%SYSTEM.Event).WaitMsg(..GetEventName(),1) if $listvalid(msg) { - set $listbuild(msgType,senderPID) = msg + set $listbuild(msgType,msgContent) = msg } quit code } @@ -94,14 +149,13 @@ ClassMethod StartInternal() set daemon = ..%New() do daemon.Run() } catch err { - do err.Log() + do LogForDaemon(err.DisplayString()) } } ClassMethod Stop() { - set deleted = ##class(%SYSTEM.Event).Delete(..GetEventName()) - w "deleted the event? ", deleted, ! + do ##class(%SYSTEM.Event).Delete(..GetEventName()) set pid = ^$LOCK(..GetEventName(), "OWNER") if (pid > 0) { do $System.Process.Terminate(pid) @@ -129,10 +183,15 @@ Method %OnClose() As %Status [ Private, ServerOnly = 1 ] quit $$$OK } -Method DaemonLogger(msg) As %Status +ClassMethod LogForDaemon(msg) As %Status +{ + do ..LogToApplicationErrors("!!!Credential Manager Deamon!!!"_$c(13,10)_msg) +} + +ClassMethod LogToApplicationErrors(msg) As %Status { try { - do LOG^%ETN("!!!Daemon Message!!!"_msg) + do LOG^%ETN(msg) set sc=$$$OK } catch err { set sc=err.AsStatus() From 2cf5b64a653d47a5d9a667921ed2d748cbed3745 Mon Sep 17 00:00:00 2001 From: Rutvik Saptarshi Date: Wed, 4 Oct 2023 11:11:11 -0400 Subject: [PATCH 05/27] credential manager now tracking token owner in private store key --- .../Git/Util/CredentialManager.cls | 10 ++- .../Git/Util/PrivateMemoryStore.cls | 81 ++++++++++--------- 2 files changed, 50 insertions(+), 41 deletions(-) diff --git a/cls/SourceControl/Git/Util/CredentialManager.cls b/cls/SourceControl/Git/Util/CredentialManager.cls index b4f781e6..a4a522fa 100644 --- a/cls/SourceControl/Git/Util/CredentialManager.cls +++ b/cls/SourceControl/Git/Util/CredentialManager.cls @@ -76,12 +76,14 @@ Method HandleMessage(msgType As %String, msgContent) do ..SendResponse(senderPID, "", "provide username") quit } + + // key that the token would be mapped from + set key = $lb(senderPID, gitUsername) if msgType = "GET" { - if i%pvtStore.KeyExists(gitUsername) { - do ..SendResponse(senderPID, i%pvtStore.Retrieve(gitUsername), "") + if i%pvtStore.KeyExists(key) { + do ..SendResponse(senderPID, i%pvtStore.Retrieve(key), "") } else { - // what is a good way to indicate that key does not exist? do ..SendResponse(senderPID, "", "key does not exist") } } elseif msgType = "SET" { @@ -89,7 +91,7 @@ Method HandleMessage(msgType As %String, msgContent) do ..SendResponse(senderPID, "", "provide git token") quit } - do i%pvtStore.Store(gitUsername, gitToken) + do i%pvtStore.Store(key, gitToken) do ..SendResponse(senderPID, gitToken, "") } } diff --git a/cls/SourceControl/Git/Util/PrivateMemoryStore.cls b/cls/SourceControl/Git/Util/PrivateMemoryStore.cls index e0bee8b1..dd8d1edb 100644 --- a/cls/SourceControl/Git/Util/PrivateMemoryStore.cls +++ b/cls/SourceControl/Git/Util/PrivateMemoryStore.cls @@ -19,52 +19,56 @@ Method %OnNew(size) As %Status [ Private, ServerOnly = 1 ] } else { set i%size = ..#defaultSize } - w !, "Size set to ", i%size, ! + set i%buffer = $zu(106,1,i%size) - quit $$$OK + return $$$OK } Method Store(key, value) { - 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 + if key = "" { + // TODO: throw a better exception + throw ##class(%Exception.General).%New("INVALID_KEY_EXCEPTION","999",, "Invalid key `""""`") + } + 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 } - // add mapping for the key - set i%map(key) = $lb(i%offset,length) - set i%offset = ..insertIntoMemoryStore(value, i%buffer, i%offset) + + 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) } Method Retrieve(key) As %RawString { - quit:('..KeyExists(key)) "" + return:('..KeyExists(key)) "" set $listbuild(offset,length) = i%map(key) return $view(i%buffer+offset,-3,-length) @@ -90,6 +94,9 @@ Method %OnClose() As %Status [ Private, ServerOnly = 1 ] Method KeyExists(key) As %Boolean { + if key = "" { + return 0 + } return '($Get(i%map(key)) = "") } @@ -101,7 +108,7 @@ Method insertIntoMemoryStore(value, buffer, offset) As %Integer [ Private ] { set length = $length(value) view buffer+offset:-3:-length:value - quit offset + length + return offset + length } Method clearBuffer() [ Private ] From 231d929621d1ea53c2a37e51eec6920fcccfb7f6 Mon Sep 17 00:00:00 2001 From: Rutvik Saptarshi Date: Wed, 4 Oct 2023 11:54:25 -0400 Subject: [PATCH 06/27] tracking owner by iris username instead of PID --- .../Git/Util/CredentialManager.cls | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/cls/SourceControl/Git/Util/CredentialManager.cls b/cls/SourceControl/Git/Util/CredentialManager.cls index a4a522fa..89b85bd6 100644 --- a/cls/SourceControl/Git/Util/CredentialManager.cls +++ b/cls/SourceControl/Git/Util/CredentialManager.cls @@ -9,13 +9,23 @@ ClassMethod Test() 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() +{ + set username = "testUser" set token = ..GetToken(username, .err, .code) zw token, err, code } @@ -42,7 +52,7 @@ Method Run() 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) - quit token + return token } ClassMethod SetToken(gitUsername As %String, gitToken As %String, Output error As %String, Output code) @@ -78,8 +88,9 @@ Method HandleMessage(msgType As %String, msgContent) } - // key that the token would be mapped from - set key = $lb(senderPID, gitUsername) + set irisUsername = $System.Process.UserName(senderPID) + // 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), "") @@ -107,7 +118,7 @@ ClassMethod Signal(msgType As %String, msgContent As %String, Output responseCod // Signal the daemon do ##class(%SYSTEM.Event).Signal(..GetEventName(),$ListBuild(msgType,msgContent)) set $listbuild(responseCode,msg) = $System.Event.WaitMsg("",5) - quit msg + return msg } Method Wait(Output msgType As %String, Output msgContent As %String) As %Integer @@ -117,7 +128,7 @@ Method Wait(Output msgType As %String, Output msgContent As %String) As %Integer if $listvalid(msg) { set $listbuild(msgType,msgContent) = msg } - quit code + return code } ClassMethod GetEventName() As %String @@ -182,7 +193,7 @@ ClassMethod CheckStatus() As %Boolean Method %OnClose() As %Status [ Private, ServerOnly = 1 ] { do ##class(%SYSTEM.Event).Delete(..GetEventName()) - quit $$$OK + return $$$OK } ClassMethod LogForDaemon(msg) As %Status @@ -198,7 +209,7 @@ ClassMethod LogToApplicationErrors(msg) As %Status } catch err { set sc=err.AsStatus() } - quit sc + return sc } } From 0244f78a30ac39b9171b2b2e7780563b052e1d48 Mon Sep 17 00:00:00 2001 From: Rutvik Saptarshi Date: Wed, 4 Oct 2023 15:50:36 -0400 Subject: [PATCH 07/27] updated how username is retreived --- cls/SourceControl/Git/Util/CredentialManager.cls | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cls/SourceControl/Git/Util/CredentialManager.cls b/cls/SourceControl/Git/Util/CredentialManager.cls index 89b85bd6..dcb6daee 100644 --- a/cls/SourceControl/Git/Util/CredentialManager.cls +++ b/cls/SourceControl/Git/Util/CredentialManager.cls @@ -88,7 +88,8 @@ Method HandleMessage(msgType As %String, msgContent) } - set irisUsername = $System.Process.UserName(senderPID) + + set irisUsername = ##class(%SYS.ProcessQuery).%OpenId(senderPID).UserName // key that the token would be mapped from set key = $lb(irisUsername, gitUsername) if msgType = "GET" { From 83e2bb9aceb82e244b4f13284508ddad2c0688f1 Mon Sep 17 00:00:00 2001 From: Rutvik Saptarshi Date: Wed, 11 Oct 2023 16:28:35 -0400 Subject: [PATCH 08/27] added some type annotations --- cls/SourceControl/Git/Util/CredentialManager.cls | 2 +- cls/SourceControl/Git/Util/PrivateMemoryStore.cls | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/cls/SourceControl/Git/Util/CredentialManager.cls b/cls/SourceControl/Git/Util/CredentialManager.cls index dcb6daee..07ad4706 100644 --- a/cls/SourceControl/Git/Util/CredentialManager.cls +++ b/cls/SourceControl/Git/Util/CredentialManager.cls @@ -67,7 +67,7 @@ ClassMethod SendResponse(toPID As %Integer, message As %String, error As %String } } -Method HandleMessage(msgType As %String, msgContent) +Method HandleMessage(msgType As %String, msgContent As %String) { try { // make sure the message is appropriately formatted diff --git a/cls/SourceControl/Git/Util/PrivateMemoryStore.cls b/cls/SourceControl/Git/Util/PrivateMemoryStore.cls index dd8d1edb..08330a48 100644 --- a/cls/SourceControl/Git/Util/PrivateMemoryStore.cls +++ b/cls/SourceControl/Git/Util/PrivateMemoryStore.cls @@ -12,7 +12,7 @@ Property size [ Internal, Private ]; Parameter defaultSize = 128; -Method %OnNew(size) As %Status [ Private, ServerOnly = 1 ] +Method %OnNew(size as %Integer) As %Status [ Private, ServerOnly = 1 ] { if $DATA(size) && $ISVALIDNUM(size) && (size >= 0) { set i%size = size @@ -104,7 +104,7 @@ Method KeyExists(key) As %Boolean // Writes to Buffer and returns new offset -Method insertIntoMemoryStore(value, buffer, offset) As %Integer [ Private ] +Method insertIntoMemoryStore(value, buffer, offset As %Integer) As %Integer [ Private ] { set length = $length(value) view buffer+offset:-3:-length:value @@ -151,8 +151,11 @@ Method deallocateBuffer() [ Private ] // 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)) From 115eaca2da5602d1059e0d0d48bc20c023faf751 Mon Sep 17 00:00:00 2001 From: Rutvik Saptarshi Date: Wed, 18 Oct 2023 14:15:50 -0400 Subject: [PATCH 09/27] token acquired --- cls/SourceControl/Git/Extension.cls | 3 +- cls/SourceControl/Git/OAuth.cls | 65 ++++++++++++ cls/SourceControl/Git/OAuth2/Config.cls | 124 ++++++++++++++++++++++ cls/SourceControl/Git/OAuth2/Endpoint.cls | 12 +++ cls/SourceControl/Git/Utils.cls | 10 ++ csp/oauth.csp | 63 +++++++++++ module.xml | 1 + 7 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 cls/SourceControl/Git/OAuth.cls create mode 100644 cls/SourceControl/Git/OAuth2/Config.cls create mode 100644 cls/SourceControl/Git/OAuth2/Endpoint.cls create mode 100644 csp/oauth.csp diff --git a/cls/SourceControl/Git/Extension.cls b/cls/SourceControl/Git/Extension.cls index 9e639c5c..55a69619 100644 --- a/cls/SourceControl/Git/Extension.cls +++ b/cls/SourceControl/Git/Extension.cls @@ -11,6 +11,7 @@ XData Menu

+ @@ -115,6 +116,7 @@ Method OnSourceMenuItem(name As %String, ByRef Enabled As %String, ByRef Display // cases "Status": 1, "GitWebUI" : 1, + "Authenticate": 1, //TODO: not always "Export": 1, "ExportForce": 1, "Import": 1, @@ -346,4 +348,3 @@ Method AddToSourceControl(InternalName As %String, Description As %String = "") } } - diff --git a/cls/SourceControl/Git/OAuth.cls b/cls/SourceControl/Git/OAuth.cls new file mode 100644 index 00000000..a571ebf4 --- /dev/null +++ b/cls/SourceControl/Git/OAuth.cls @@ -0,0 +1,65 @@ +Class SourceControl.Git.OAuth Extends %RegisteredObject +{ + +Property ClientID As %String; + +Property ClientSecret As %String; + +Method %OnNew() As %Status +{ + set i%ClientSecret = "b445a726515185870c14bef647dc327ad688726b" + set i%ClientID = "b56623587367a6502ceb" +} + +// GenerateVerifier returns a cryptographically random 32 byte value + +ClassMethod GenerateVerifier() As %String +{ + new $NAMESPACE + set $NAMESPACE = "%SYS" + return ##class(%SYSTEM.Encryption).GenCryptRand(32) +} + +ClassMethod AuthCodeURLForGithub(namespace As %String, Output state, Output verifier) As %String +{ + // (clientID As %String, clientSecret As %String, authEndpoint As %String, tokenEndpoint As %String, redirectURL As %String, scopes As %String) As %Status + set config = ##class(SourceControl.Git.OAuth2.Config).%New( + "b56623587367a6502ceb", + "b445a726515185870c14bef647dc327ad688726b", + "https://github.com/login/oauth/authorize", + "https://github.com/login/oauth/access_token" , + "http://localhost:52773/isc/studio/usertemplates/gitsourcecontrol/oauth.csp/", + $lb("repo")) + + return ..AuthCodeURL(config, namespace, .state, .verifier) +} + +ClassMethod ExchangeForGithub(authCode As %String, verifier As %String, Output sc As %Status) As %String +{ + set config = ##class(SourceControl.Git.OAuth2.Config).%New( + "b56623587367a6502ceb", + "b445a726515185870c14bef647dc327ad688726b", + "https://github.com/login/oauth/authorize", + "https://github.com/login/oauth/access_token", + "http://localhost:52773/isc/studio/usertemplates/gitsourcecontrol/oauth.csp/", + $lb("repo")) + return config.Exchange(authCode, verifier, .sc) +} + +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 Configure() As %Status +{ +} + +Method FetchCredentials(username As %String) +{ +} + +} diff --git a/cls/SourceControl/Git/OAuth2/Config.cls b/cls/SourceControl/Git/OAuth2/Config.cls new file mode 100644 index 00000000..44a5fff8 --- /dev/null +++ b/cls/SourceControl/Git/OAuth2/Config.cls @@ -0,0 +1,124 @@ +Class SourceControl.Git.OAuth2.Config Extends %RegisteredObject +{ + +// ClientID is the OAuth Application ID + +Property ClientID As %String; + +// ClientSecret is the OAuth Application secret + +Property ClientSecret As %String; + +// 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; + +// Scopes specifies the list of scopes we are requesting access to + +Property Scopes; + +// TODO: We will need a authStyleCache when we use autodetect for Endpoint.AuthStyle in the future + +Method %OnNew(clientID As %String, clientSecret As %String, authEndpoint As %String, tokenEndpoint As %String, redirectURL As %String, scopes As %String) As %Status +{ + set ..ClientID = clientID + set ..ClientSecret = clientSecret + set ..Endpoint = ##class(Endpoint).%New() + set ..Endpoint.AuthURL = authEndpoint + set ..Endpoint.TokenURL = tokenEndpoint + set ..RedirectURL = redirectURL + 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 + + // TODO: also add support to put client creds in header instead of params + // either is allowed, will have to try both to see which succeeds + // this is when we will need the `AuthStyle` parameter in the endpoint + do request.SetHeader("Accept", "application/json") + + 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 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 +} + +} diff --git a/cls/SourceControl/Git/OAuth2/Endpoint.cls b/cls/SourceControl/Git/OAuth2/Endpoint.cls new file mode 100644 index 00000000..1a55d075 --- /dev/null +++ b/cls/SourceControl/Git/OAuth2/Endpoint.cls @@ -0,0 +1,12 @@ +Class SourceControl.Git.OAuth2.Endpoint Extends %RegisteredObject +{ + +Property AuthURL As %String; + +Property DeviceAuthURL As %String; + +Property TokenURL As %String; + +// TODO: Might also need a Property to describe the auth style (i.e either in the header, or in the body) + +} diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index 50a34842..c9a8ff5b 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -187,6 +187,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/oauth.csp/"_$namespace_"/?"_urlPostfix } elseif (menuItemName = "Export") || (menuItemName = "ExportForce") { write !, "==export start==",! set ec = ..ExportAll($case(menuItemName="ExportForce",1:$$$Force,:0)) @@ -1989,4 +1992,11 @@ ClassMethod ResetSourceControlClass() do ##class(%Studio.SourceControl.Interface).SourceControlClassSet("") } +ClassMethod WriteLineToFile(filePath As %String, line As %String) +{ + Set file=##class(%File).%New(filePath) + Do file.Open("WSN") + Do file.WriteLine(line) +} + } diff --git a/csp/oauth.csp b/csp/oauth.csp new file mode 100644 index 00000000..ee7ce6d0 --- /dev/null +++ b/csp/oauth.csp @@ -0,0 +1,63 @@ + + + if $Data(%request.Data("state",1),state)#2 { + + if '$Data(%session.Data("state"))#2 { + w "1 sessId: "_%session.SessionId + w "
"_$Get(%session.Data("state")) + quit 1 + } elseif (%session.Data("state") '= state){ + w "2" + quit 1 + } + + if '$Data(%request.Data("code",1),code)#2 { + w "Bad request: Invalid parameters" + quit 1 + } + + set verifier = $select($Data(%request.Data("verifier",1),verifier)#2: verifier, 1: "") + + // switch to the namespace that the extension is installed to + set namespace = $Piece(state,"_",1) + new $Namespace + set $Namespace = namespace + + set result = ##class(SourceControl.Git.OAuth).ExchangeForGithub(code, verifier, .sc) + if sc '= $$$OK { + do SYSTEM.Status.DisplayError(sc) + w "
" + w "Unable to retreive access token" + } + + + // https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=GNET_http + + } else { + w "no state lol" + } +
\ No newline at end of file diff --git a/module.xml b/module.xml index a05a04d8..26a10fd5 100644 --- a/module.xml +++ b/module.xml @@ -24,6 +24,7 @@ + From acd230d7a90f3a15ad7e133c498056c335129f25 Mon Sep 17 00:00:00 2001 From: isc-tleavitt <73311181+isc-tleavitt@users.noreply.github.com> Date: Wed, 18 Oct 2023 14:20:28 -0400 Subject: [PATCH 10/27] Add prerelease designation (for ongoing work in this branch) --- module.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module.xml b/module.xml index 26a10fd5..294d9616 100644 --- a/module.xml +++ b/module.xml @@ -3,7 +3,7 @@ git-source-control - 2.3.0 + 2.3.0-oauth.1 Server-side source control extension for use of Git on InterSystems platforms git source control studio vscode module From 45f96a9fc6bbac960b989041ca32024fd7b1e204 Mon Sep 17 00:00:00 2001 From: isc-tleavitt <73311181+isc-tleavitt@users.noreply.github.com> Date: Wed, 18 Oct 2023 15:04:49 -0400 Subject: [PATCH 11/27] Fix compilation error --- cls/SourceControl/Git/Util/CredentialManager.cls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cls/SourceControl/Git/Util/CredentialManager.cls b/cls/SourceControl/Git/Util/CredentialManager.cls index 07ad4706..e76e515b 100644 --- a/cls/SourceControl/Git/Util/CredentialManager.cls +++ b/cls/SourceControl/Git/Util/CredentialManager.cls @@ -163,7 +163,7 @@ ClassMethod StartInternal() set daemon = ..%New() do daemon.Run() } catch err { - do LogForDaemon(err.DisplayString()) + do ..LogForDaemon(err.DisplayString()) } } From 4df9c82feffa56e9160b14abeefce0da035b598f Mon Sep 17 00:00:00 2001 From: Rutvik Saptarshi Date: Fri, 20 Oct 2023 16:44:04 -0400 Subject: [PATCH 12/27] final end of rotation commit --- cls/SourceControl/Git/OAuth.cls | 65 ---------- cls/SourceControl/Git/OAuth2.cls | 113 ++++++++++++++++++ cls/SourceControl/Git/OAuth2/Config.cls | 82 ++++++++++--- cls/SourceControl/Git/OAuth2/Endpoint.cls | 21 +++- cls/SourceControl/Git/Settings.cls | 49 +++++++- .../Git/Util/CredentialManager.cls | 64 +++++----- .../Git/Util/PrivateMemoryStore.cls | 20 +++- cls/SourceControl/Git/Utils.cls | 2 +- csp/{oauth.csp => oauth2.csp} | 16 +-- module.xml | 2 +- 10 files changed, 300 insertions(+), 134 deletions(-) delete mode 100644 cls/SourceControl/Git/OAuth.cls create mode 100644 cls/SourceControl/Git/OAuth2.cls rename csp/{oauth.csp => oauth2.csp} (75%) diff --git a/cls/SourceControl/Git/OAuth.cls b/cls/SourceControl/Git/OAuth.cls deleted file mode 100644 index a571ebf4..00000000 --- a/cls/SourceControl/Git/OAuth.cls +++ /dev/null @@ -1,65 +0,0 @@ -Class SourceControl.Git.OAuth Extends %RegisteredObject -{ - -Property ClientID As %String; - -Property ClientSecret As %String; - -Method %OnNew() As %Status -{ - set i%ClientSecret = "b445a726515185870c14bef647dc327ad688726b" - set i%ClientID = "b56623587367a6502ceb" -} - -// GenerateVerifier returns a cryptographically random 32 byte value - -ClassMethod GenerateVerifier() As %String -{ - new $NAMESPACE - set $NAMESPACE = "%SYS" - return ##class(%SYSTEM.Encryption).GenCryptRand(32) -} - -ClassMethod AuthCodeURLForGithub(namespace As %String, Output state, Output verifier) As %String -{ - // (clientID As %String, clientSecret As %String, authEndpoint As %String, tokenEndpoint As %String, redirectURL As %String, scopes As %String) As %Status - set config = ##class(SourceControl.Git.OAuth2.Config).%New( - "b56623587367a6502ceb", - "b445a726515185870c14bef647dc327ad688726b", - "https://github.com/login/oauth/authorize", - "https://github.com/login/oauth/access_token" , - "http://localhost:52773/isc/studio/usertemplates/gitsourcecontrol/oauth.csp/", - $lb("repo")) - - return ..AuthCodeURL(config, namespace, .state, .verifier) -} - -ClassMethod ExchangeForGithub(authCode As %String, verifier As %String, Output sc As %Status) As %String -{ - set config = ##class(SourceControl.Git.OAuth2.Config).%New( - "b56623587367a6502ceb", - "b445a726515185870c14bef647dc327ad688726b", - "https://github.com/login/oauth/authorize", - "https://github.com/login/oauth/access_token", - "http://localhost:52773/isc/studio/usertemplates/gitsourcecontrol/oauth.csp/", - $lb("repo")) - return config.Exchange(authCode, verifier, .sc) -} - -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 Configure() As %Status -{ -} - -Method FetchCredentials(username As %String) -{ -} - -} diff --git a/cls/SourceControl/Git/OAuth2.cls b/cls/SourceControl/Git/OAuth2.cls new file mode 100644 index 00000000..827cb782 --- /dev/null +++ b/cls/SourceControl/Git/OAuth2.cls @@ -0,0 +1,113 @@ +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) +} + +/// Gets the AuthCodeURL for github +/// namespace: is the name of the namespace that git source control is installed in +/// (we need this to ensure that we switch to the right namespace when we get the authcode from the server) +/// state -- Output : random 32 byte string to validate authCode redirects +/// verifier -- Output : random 32 byte string to validate during token exchange +ClassMethod AuthCodeURLForGithub(namespace As %String, Output state, Output verifier) As %String +{ + // (clientID As %String, clientSecret As %String, authEndpoint As %String, tokenEndpoint As %String, redirectURL As %String, scopes As %String) As %Status + set config = ##class(SourceControl.Git.OAuth2.Config).%New( + "Github", + "b56623587367a6502ceb", + "b445a726515185870c14bef647dc327ad688726b", + "https://github.com/login/oauth/authorize", + "https://github.com/login/oauth/access_token" , + ..GetOAuthRedirectEndpoint(), + $lb("repo")) + + return ..AuthCodeURL(config, namespace, .state, .verifier) +} + +/// Exchanges an authorization code and associated verifier with the server and returns the retreived access_token +/// authCode: Authorization code received from github after user authenticates +/// verifier: PKCE verifier whose hash was sent to github via the authCodeURL +ClassMethod ExchangeForGithub(authCode As %String, verifier As %String, Output sc As %Status) As %String +{ + set config = ##class(SourceControl.Git.OAuth2.Config).%New( + "Github", + "b56623587367a6502ceb", + "b445a726515185870c14bef647dc327ad688726b", + "https://github.com/login/oauth/authorize", + "https://github.com/login/oauth/access_token", + ..GetOAuthRedirectEndpoint(), + $lb("repo")) + return config.Exchange(authCode, verifier, .sc) +} + +/// 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 +} + +/// Configures all the settings requried to retreive access_tokens with oauth2 +ClassMethod Configure() As %Status +{ + set defaultPromptFlag = $$$DisableBackupCharMask + $$$TrapCtrlCMask + $$$EnableQuitCharMask + $$$DisableHelpCharMask + $$$DisableHelpContextCharMask + $$$TrapErrorMask + // get authURL + Write !, "OAuth2 Configuration", ! + + set response = ##class(%Library.Prompt).GetString("Enter name for client configuration:",.configName,,,,defaultPromptFlag) + if (response '= $$$SuccessResponse) || configName = "" { + return $$$ERROR($$$GeneralError,"Error occured when reading configuration name") + } + + set redirectURL = ..GetOAuthRedirectEndpoint() + w !, "Please configure an OAuth application with your git provider", ! + w "Be sure to whitelist this redirect URL: "_redirectURL, ! + w "Once configured, enter the following details:", ! + + // get authURL + set response = ##class(%Library.Prompt).GetString("Auth Code URL:",.authCodeURL,,,,defaultPromptFlag) + if (response '= $$$SuccessResponse) { + return $$$ERROR($$$GeneralError,"Error occured when reading Auth Code URL") + } + + // get tokenURL + set response = ##class(%Library.Prompt).GetString("Token URL:",.tokenURL,,,,defaultPromptFlag) + if (response '= $$$SuccessResponse) { + return $$$ERROR($$$GeneralError,"Error occured when reading Token URL") + } + + // get clientID + set response = ##class(%Library.Prompt).GetString("ClientID:",.clientID,,,,defaultPromptFlag) + if (response '= $$$SuccessResponse) { + return $$$ERROR($$$GeneralError,"Error occured when reading ClientID") + } + // get clientSecret + set response = ##class(%Library.Prompt).GetString("ClientSecret:",.clientSecret,,,,defaultPromptFlag) + if (response '= $$$SuccessResponse) { + return $$$ERROR($$$GeneralError,"Error occured when reading ClientSecret") + } + + set config = ##class(SourceControl.Git.OAuth2.Config).%New(configName, clientID, clientSecret, authCodeURL, tokenURL, redirectURL) +} + +/// Returns the full URL for the oauth2.csp endpoint +ClassMethod GetOAuthRedirectEndpoint() As %String +{ + // TODO: make this dynamic + set redirectHost = "http://localhost:52773" + set redirectPath = "/isc/studio/usertemplates/gitsourcecontrol/oauth2.csp" + return redirectHost_redirectPath +} + +} diff --git a/cls/SourceControl/Git/OAuth2/Config.cls b/cls/SourceControl/Git/OAuth2/Config.cls index 44a5fff8..d9201acc 100644 --- a/cls/SourceControl/Git/OAuth2/Config.cls +++ b/cls/SourceControl/Git/OAuth2/Config.cls @@ -1,32 +1,32 @@ Class SourceControl.Git.OAuth2.Config Extends %RegisteredObject { -// ClientID is the OAuth Application ID +/// Name is the identifier for this configuration +Property Name As %String(MAXLEN = 127); -Property ClientID As %String; +/// ClientID is the OAuth Application ID +Property ClientID As %String(MAXLEN = ""); -// ClientSecret is the OAuth Application secret - -Property ClientSecret As %String; - -// Endpoint contains the resource server's token endpoint +/// ClientSecret is the OAuth Application secret +Property ClientSecret As %String(MAXLEN = ""); +/// 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; - -// Scopes specifies the list of scopes we are requesting access to +/// RedirectURL is the URL to redirect the auth token +/// to after authenticating with the resource owner +Property RedirectURL As %String(MAXLEN = ""); +/// Scopes specifies the list of scopes we are requesting access to Property Scopes; +Index NameID On Name [ IdKey ]; + // TODO: We will need a authStyleCache when we use autodetect for Endpoint.AuthStyle in the future -Method %OnNew(clientID As %String, clientSecret As %String, authEndpoint As %String, tokenEndpoint As %String, redirectURL As %String, scopes As %String) As %Status +Method %OnNew(configName As %String, clientID As %String, clientSecret As %String, authEndpoint As %String, tokenEndpoint As %String, redirectURL As %String, scopes As %String) As %Status { + set ..Name = configName set ..ClientID = clientID set ..ClientSecret = clientSecret set ..Endpoint = ##class(Endpoint).%New() @@ -121,4 +121,56 @@ ClassMethod GetURLWithParams(url As %String, ByRef params As %String) As %String return url } +Storage Default +{ + + +%%CLASSNAME + + +ClientID + + +ClientSecret + + +Endpoint + + +RedirectURL + + +Scopes + + + + +Name + + +ClientID + + +ClientSecret + + +Endpoint + + +RedirectURL + + +Scopes + + +^SourceControl.Git.O7826.ConfigD +ConfigDefaultData +^SourceControl.Git.O7826.ConfigD +^SourceControl.Git.O7826.ConfigI +ConfigState +^SourceControl.Git.O7826.ConfigS +%Storage.Serial } + +} + diff --git a/cls/SourceControl/Git/OAuth2/Endpoint.cls b/cls/SourceControl/Git/OAuth2/Endpoint.cls index 1a55d075..599f14fe 100644 --- a/cls/SourceControl/Git/OAuth2/Endpoint.cls +++ b/cls/SourceControl/Git/OAuth2/Endpoint.cls @@ -1,4 +1,4 @@ -Class SourceControl.Git.OAuth2.Endpoint Extends %RegisteredObject +Class SourceControl.Git.OAuth2.Endpoint Extends %SerialObject { Property AuthURL As %String; @@ -9,4 +9,23 @@ Property TokenURL As %String; // TODO: Might also need a Property to describe the auth style (i.e either in the header, or in the body) +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 5e3ed17a..6828b8c8 100644 --- a/cls/SourceControl/Git/Settings.cls +++ b/cls/SourceControl/Git/Settings.cls @@ -26,6 +26,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()} ]; +/// Type of git remote +Property gitRemoteType As %String(VALUELIST = ",HTTPS,SSH"); + +/// URL for git remote +Property gitRemoteURL As %String(MAXLEN = ""); + Property Mappings [ MultiDimensional ]; Method %OnNew() As %Status @@ -155,16 +161,53 @@ Method OnAfterConfigure() As %Boolean set workMgr = $System.WorkMgr.%New("") $$$ThrowOnError(workMgr.Queue("##class(SourceControl.Git.Utils).Init")) $$$ThrowOnError(workMgr.Sync()) - do ##class(SourceControl.Git.Utils).EmptyInitialCommit() + do ##class(Utils).EmptyInitialCommit() } elseif (value = 2) { - set response = ##class(%Library.Prompt).GetString("Git remote URL (note: if authentication is required, use SSH, not HTTPS):",.remote,,,,defaultPromptFlag) + set remoteTypes("1") = "HTTPS (Only remotes with OAuth2 support)" + set remoteTypes("2") = "SSH" + set response = ##class(%Library.Prompt).GetString("Git remote type:",.remoteType,.remoteTypes,,,defaultPromptFlag + $$$InitialDisplayMask) + if (response '= $$$SuccessResponse) { + quit + } + + set ..gitRemoteType = $select((remoteType = "1"): "HTTPS", 1: "SSH") + + 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 + + if ..gitRemoteType = "HTTPS" { + do ##class(OAuth2).Configure() + set redirectURL = ##class(OAuth2).GetOAuthRedirectEndpoint()_"/"_$NAMESPACE + Write "Please navigate to "_" on your browser to login to the git server", ! + + + // 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(, .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.Sync()) diff --git a/cls/SourceControl/Git/Util/CredentialManager.cls b/cls/SourceControl/Git/Util/CredentialManager.cls index 07ad4706..825076fd 100644 --- a/cls/SourceControl/Git/Util/CredentialManager.cls +++ b/cls/SourceControl/Git/Util/CredentialManager.cls @@ -4,7 +4,7 @@ Class SourceControl.Git.Util.CredentialManager Extends %RegisteredObject /// Description Property pvtStore [ Internal, Private ]; -ClassMethod Test() +ClassMethod Test() [ Private ] { Do ##class(SourceControl.Git.Util.CredentialManager).Stop() @@ -23,14 +23,16 @@ ClassMethod Test() zw token, err, code } -ClassMethod Test2() +ClassMethod Test2() [ Private ] { set username = "testUser" set token = ..GetToken(username, .err, .code) zw token, err, code } -Method Run() +/// Creates the `..GetEventName()` named event +/// Waits on signals and services request +Method Run() [ Private ] { do ##class(%SYSTEM.Event).Create(..GetEventName()) @@ -40,34 +42,42 @@ Method Run() try { set code = ..Wait(.msgType, .msgContent) if (code = 1) { - do ..LogForDaemon("Code: "_code_" "_"Received "_msgType_" && "_ " "_msgContent) do ..HandleMessage(msgType, msgContent) } } catch err { - do err.Log() + do err.Log() } } } -ClassMethod GetToken(gitUsername As %String, Output error As %String, Output code As %String) As %String +/// 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 } -ClassMethod SetToken(gitUsername As %String, gitToken As %String, Output error As %String, Output code) +/// SetToken is used to retreive 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) +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) + #; do ..LogForDaemon("Unable to send message: """_message_""" to: "_toPID) } } -Method HandleMessage(msgType As %String, msgContent As %String) +Method HandleMessage(msgType As %String, msgContent As %String) [ Private ] { try { // make sure the message is appropriately formatted @@ -78,7 +88,7 @@ Method HandleMessage(msgType As %String, msgContent As %String) } if '$data(senderPID) { - do ..LogForDaemon("No source PID provided") + #; do ..LogForDaemon("No source PID provided") quit } @@ -108,7 +118,7 @@ Method HandleMessage(msgType As %String, msgContent As %String) } } -ClassMethod Signal(msgType As %String, msgContent As %String, Output responseCode) As %String +ClassMethod Signal(msgType As %String, msgContent As %String, Output responseCode) As %String [ Private ] { // Make sure the daemon is running do ..Start() @@ -132,12 +142,12 @@ Method Wait(Output msgType As %String, Output msgContent As %String) As %Integer return code } -ClassMethod GetEventName() As %String +ClassMethod GetEventName() As %String [ Private ] { return $Name(^isc.git.sc("Daemon")) //^"_$classname() } -ClassMethod Start() +ClassMethod Start() [ Private ] { if ..CheckStatus() { quit @@ -156,18 +166,18 @@ ClassMethod Start() } } -ClassMethod StartInternal() +ClassMethod StartInternal() [ Private ] { try { set lock = $System.AutoLock.Lock(..GetEventName(), , 2) set daemon = ..%New() do daemon.Run() } catch err { - do LogForDaemon(err.DisplayString()) + #; do LogForDaemon(err.DisplayString()) } } -ClassMethod Stop() +ClassMethod Stop() [ Private ] { do ##class(%SYSTEM.Event).Delete(..GetEventName()) set pid = ^$LOCK(..GetEventName(), "OWNER") @@ -176,13 +186,13 @@ ClassMethod Stop() } } -ClassMethod Restart() +ClassMethod Restart() [ Private ] { do ..Stop() do ..Start() } -ClassMethod CheckStatus() As %Boolean +ClassMethod CheckStatus() As %Boolean [ Private ] { return ($data(^$LOCK(..GetEventName())) = 10) } @@ -197,20 +207,4 @@ Method %OnClose() As %Status [ Private, ServerOnly = 1 ] return $$$OK } -ClassMethod LogForDaemon(msg) As %Status -{ - do ..LogToApplicationErrors("!!!Credential Manager Deamon!!!"_$c(13,10)_msg) -} - -ClassMethod LogToApplicationErrors(msg) As %Status -{ - try { - do LOG^%ETN(msg) - set sc=$$$OK - } catch err { - set sc=err.AsStatus() - } - return sc -} - } diff --git a/cls/SourceControl/Git/Util/PrivateMemoryStore.cls b/cls/SourceControl/Git/Util/PrivateMemoryStore.cls index 08330a48..e13e6666 100644 --- a/cls/SourceControl/Git/Util/PrivateMemoryStore.cls +++ b/cls/SourceControl/Git/Util/PrivateMemoryStore.cls @@ -12,7 +12,7 @@ Property size [ Internal, Private ]; Parameter defaultSize = 128; -Method %OnNew(size as %Integer) As %Status [ Private, ServerOnly = 1 ] +Method %OnNew(size As %Integer) As %Status [ Private, ServerOnly = 1 ] { if $DATA(size) && $ISVALIDNUM(size) && (size >= 0) { set i%size = size @@ -24,7 +24,9 @@ Method %OnNew(size as %Integer) As %Status [ Private, ServerOnly = 1 ] return $$$OK } -Method Store(key, value) +/// Store `key`: `value` to the map +/// throws an exception if key = "" +Method Store(key As %String, value As %String) { if key = "" { // TODO: throw a better exception @@ -66,7 +68,9 @@ Method Store(key, value) set i%offset = ..insertIntoMemoryStore(value, i%buffer, i%offset) } -Method Retrieve(key) As %RawString +/// Retreives the value associated with `key` +/// Returns `""` if key does not exist +Method Retrieve(key As %String) As %RawString { return:('..KeyExists(key)) "" @@ -74,7 +78,9 @@ Method Retrieve(key) As %RawString return $view(i%buffer+offset,-3,-length) } -Method Clear(key) +/// 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)) @@ -92,12 +98,14 @@ Method %OnClose() As %Status [ Private, ServerOnly = 1 ] do ..deallocateBuffer() } -Method KeyExists(key) As %Boolean +/// 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)) = "") + return $Get(i%map(key)) '= "" } // PRIVATE METHODS ====> diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index c9a8ff5b..252a957c 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -189,7 +189,7 @@ ClassMethod UserAction(InternalName As %String, MenuName As %String, ByRef Targe 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/oauth.csp/"_$namespace_"/?"_urlPostfix + set Target = urlPrefix _ "/isc/studio/usertemplates/gitsourcecontrol/oauth2.csp/"_$namespace_"/?"_urlPostfix } elseif (menuItemName = "Export") || (menuItemName = "ExportForce") { write !, "==export start==",! set ec = ..ExportAll($case(menuItemName="ExportForce",1:$$$Force,:0)) diff --git a/csp/oauth.csp b/csp/oauth2.csp similarity index 75% rename from csp/oauth.csp rename to csp/oauth2.csp index ee7ce6d0..ff50858a 100644 --- a/csp/oauth.csp +++ b/csp/oauth2.csp @@ -14,12 +14,9 @@ set $Namespace = namespace if '$Data(%request.Data("code",1),code)#2 { - set %response.Redirect = ##class(SourceControl.Git.OAuth).AuthCodeURLForGithub(namespace,.state,.verifier) + set %response.Redirect = ##class(SourceControl.Git.OAuth2).AuthCodeURLForGithub(namespace,.state,.verifier) set %session.Data("state") = state set %session.Data("verifier") = verifier - - do ##class(SourceControl.Git.Utils).WriteLineToFile("c:\Users\rsaptars\Desktop\firstSessionId.txt", %session.SessionId) - do ##class(SourceControl.Git.Utils).WriteLineToFile("c:\Users\rsaptars\Desktop\initialState.txt", %session.Data("state")) } quit 1 @@ -47,17 +44,22 @@ new $Namespace set $Namespace = namespace - set result = ##class(SourceControl.Git.OAuth).ExchangeForGithub(code, verifier, .sc) + set result = ##class(SourceControl.Git.OAuth2).ExchangeForGithub(code, verifier, .sc) if sc '= $$$OK { do SYSTEM.Status.DisplayError(sc) w "
" w "Unable to retreive access token" + } else { + do ##class(SourceControl.Git.Util.CredentialManager).SetToken(, .err, .code) + if (code '= 1) || (err '= "") { + w "Unable to save credential" + } else { + w "configured!" + } } // https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=GNET_http - } else { - w "no state lol" } \ No newline at end of file diff --git a/module.xml b/module.xml index 26a10fd5..932ebbdd 100644 --- a/module.xml +++ b/module.xml @@ -24,7 +24,7 @@ - + From 258209efb7cc7e851eae1632cc0930a010c43be3 Mon Sep 17 00:00:00 2001 From: Elijah Tamarchenko Date: Wed, 19 Feb 2025 14:14:37 -0500 Subject: [PATCH 13/27] Initial changes Base set of changes to get authentication working properly. The Authenticate command should now follow proper OAuth2 flow and store the token for future use with git commands. Still missing: - initialConfigure working properly - Storing secrets securely - Pretty UI - alerts on error - other improvements. - thorough testing --- cls/SourceControl/Git/OAuth2.cls | 125 +++++++---- cls/SourceControl/Git/OAuth2/Config.cls | 83 +++++-- cls/SourceControl/Git/OAuth2/Endpoint.cls | 1 - cls/SourceControl/Git/Settings.cls | 27 ++- .../Git/Util/CredentialManager.cls | 3 +- cls/SourceControl/Git/Utils.cls | 24 +- csp/oauth2.csp | 205 ++++++++++++++---- 7 files changed, 353 insertions(+), 115 deletions(-) diff --git a/cls/SourceControl/Git/OAuth2.cls b/cls/SourceControl/Git/OAuth2.cls index 827cb782..56527e7e 100644 --- a/cls/SourceControl/Git/OAuth2.cls +++ b/cls/SourceControl/Git/OAuth2.cls @@ -23,8 +23,8 @@ ClassMethod AuthCodeURLForGithub(namespace As %String, Output state, Output veri // (clientID As %String, clientSecret As %String, authEndpoint As %String, tokenEndpoint As %String, redirectURL As %String, scopes As %String) As %Status set config = ##class(SourceControl.Git.OAuth2.Config).%New( "Github", - "b56623587367a6502ceb", - "b445a726515185870c14bef647dc327ad688726b", + "Ov23lic1u2IDhK6R5m1Y", + "2a84f4846510359697bf0b5f3288330c6a7fe995", "https://github.com/login/oauth/authorize", "https://github.com/login/oauth/access_token" , ..GetOAuthRedirectEndpoint(), @@ -40,8 +40,8 @@ ClassMethod ExchangeForGithub(authCode As %String, verifier As %String, Output s { set config = ##class(SourceControl.Git.OAuth2.Config).%New( "Github", - "b56623587367a6502ceb", - "b445a726515185870c14bef647dc327ad688726b", + "Ov23lic1u2IDhK6R5m1Y", + "2a84f4846510359697bf0b5f3288330c6a7fe995", "https://github.com/login/oauth/authorize", "https://github.com/login/oauth/access_token", ..GetOAuthRedirectEndpoint(), @@ -59,55 +59,96 @@ ClassMethod AuthCodeURL(c As SourceControl.Git.OAuth2.Config, namespace As %Stri } /// Configures all the settings requried to retreive access_tokens with oauth2 -ClassMethod Configure() As %Status +ClassMethod Configure(remote As %String = "") As SourceControl.Git.OAuth2.Config { - set defaultPromptFlag = $$$DisableBackupCharMask + $$$TrapCtrlCMask + $$$EnableQuitCharMask + $$$DisableHelpCharMask + $$$DisableHelpContextCharMask + $$$TrapErrorMask - // get authURL - Write !, "OAuth2 Configuration", ! - - set response = ##class(%Library.Prompt).GetString("Enter name for client configuration:",.configName,,,,defaultPromptFlag) - if (response '= $$$SuccessResponse) || configName = "" { - return $$$ERROR($$$GeneralError,"Error occured when reading configuration name") - } - - set redirectURL = ..GetOAuthRedirectEndpoint() - w !, "Please configure an OAuth application with your git provider", ! - w "Be sure to whitelist this redirect URL: "_redirectURL, ! - w "Once configured, enter the following details:", ! - - // get authURL - set response = ##class(%Library.Prompt).GetString("Auth Code URL:",.authCodeURL,,,,defaultPromptFlag) - if (response '= $$$SuccessResponse) { - return $$$ERROR($$$GeneralError,"Error occured when reading Auth Code URL") - } + // TODO-etamarch -> This needs way more explanation of what is what. Maybe autodetect if using + // github / gitlab and point to necessary information / docs / explain some of the inputs - // get tokenURL - set response = ##class(%Library.Prompt).GetString("Token URL:",.tokenURL,,,,defaultPromptFlag) - if (response '= $$$SuccessResponse) { - return $$$ERROR($$$GeneralError,"Error occured when reading Token URL") - } + set config = ##class(SourceControl.Git.OAuth2.Config).GetConfig($username) - // get clientID - set response = ##class(%Library.Prompt).GetString("ClientID:",.clientID,,,,defaultPromptFlag) - if (response '= $$$SuccessResponse) { - return $$$ERROR($$$GeneralError,"Error occured when reading ClientID") - } - // get clientSecret - set response = ##class(%Library.Prompt).GetString("ClientSecret:",.clientSecret,,,,defaultPromptFlag) - if (response '= $$$SuccessResponse) { - return $$$ERROR($$$GeneralError,"Error occured when reading ClientSecret") + if (config = "") { + set defaultPromptFlag = $$$DisableBackupCharMask + $$$TrapCtrlCMask + $$$EnableQuitCharMask + $$$DisableHelpCharMask + $$$DisableHelpContextCharMask + $$$TrapErrorMask + // get authURL + Write !, "OAuth2 Configuration", ! + + set response = ##class(%Library.Prompt).GetString("Enter name for client configuration:",.configName,,,,defaultPromptFlag) + if (response '= $$$SuccessResponse) || configName = "" { + return $$$ERROR($$$GeneralError,"Error occured when reading configuration name") + } + + set redirectURL = ..GetOAuthRedirectEndpoint() + w !, "Please configure an OAuth application with your git provider", ! + w "Be sure to whitelist this redirect URL: "_redirectURL, ! + w "Once configured, enter the following details:", ! + + set urls = ..GetURLsFromRemote(remote, .authCodeURL, .tokenURL) + + if ('urls) { + + // get authURL + set response = ##class(%Library.Prompt).GetString("Auth Code URL:",.authCodeURL,,,,defaultPromptFlag) + if (response '= $$$SuccessResponse) { + return $$$ERROR($$$GeneralError,"Error occured when reading Auth Code URL") + } + + // get tokenURL + set response = ##class(%Library.Prompt).GetString("Token URL:",.tokenURL,,,,defaultPromptFlag) + if (response '= $$$SuccessResponse) { + return $$$ERROR($$$GeneralError,"Error occured when reading Token URL") + } + } + + // get clientID + set response = ##class(%Library.Prompt).GetString("ClientID:",.clientID,,,,defaultPromptFlag) + if (response '= $$$SuccessResponse) { + return $$$ERROR($$$GeneralError,"Error occured when reading ClientID") + } + // get clientSecret + set response = ##class(%Library.Prompt).GetString("ClientSecret:",.clientSecret,,,,defaultPromptFlag) + if (response '= $$$SuccessResponse) { + return $$$ERROR($$$GeneralError,"Error occured when reading ClientSecret") + } + set config = ##class(SourceControl.Git.OAuth2.Config).%New(configName, clientID, clientSecret, authCodeURL, tokenURL, redirectURL) } - - set config = ##class(SourceControl.Git.OAuth2.Config).%New(configName, clientID, clientSecret, authCodeURL, tokenURL, redirectURL) + return config } /// Returns the full URL for the oauth2.csp endpoint ClassMethod GetOAuthRedirectEndpoint() As %String { - // TODO: make this dynamic - set redirectHost = "http://localhost:52773" + // TODO-etamarch: make this dynamic + // How do we get the hostname???? + set redirectHost = "http://localhost:52776" set redirectPath = "/isc/studio/usertemplates/gitsourcecontrol/oauth2.csp" return redirectHost_redirectPath } +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 GetRemoteURLWithToken(token As %String) As %String +{ + set remote = "" + set url = ##class(SourceControl.Git.Utils).GitRemoteURL() + if url [ "//github.com/" { + set remote = "https://"_token_"@github.com"_$piece(url, "github.com", 2) + } elseif url [ "gitlab" { + set remote = "https://oauth2:"_token_"@gitlab.com"_$piece(url,"https://",2) + } + return remote +} + } diff --git a/cls/SourceControl/Git/OAuth2/Config.cls b/cls/SourceControl/Git/OAuth2/Config.cls index d9201acc..9cb1ccd1 100644 --- a/cls/SourceControl/Git/OAuth2/Config.cls +++ b/cls/SourceControl/Git/OAuth2/Config.cls @@ -1,4 +1,4 @@ -Class SourceControl.Git.OAuth2.Config Extends %RegisteredObject +Class SourceControl.Git.OAuth2.Config Extends %Persistent { /// Name is the identifier for this configuration @@ -17,14 +17,29 @@ Property Endpoint As Endpoint; /// 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; +Property Scopes As %List; + +Property Username As %String; Index NameID On Name [ IdKey ]; +Index Username On Username [ Unique ]; + +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 %String) As %Status +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 ..ClientID = clientID @@ -33,6 +48,10 @@ Method %OnNew(configName As %String, clientID As %String, clientSecret As %Strin set ..Endpoint.AuthURL = authEndpoint set ..Endpoint.TokenURL = tokenEndpoint set ..RedirectURL = redirectURL + + if ('scopes) { + set scopes = $lb("repo") + } set ..Scopes = scopes return $$$OK @@ -76,6 +95,8 @@ Method Exchange(authCode As %String, verifier As %String, Output sc As %Status) // this is when we will need the `AuthStyle` parameter in the endpoint do request.SetHeader("Accept", "application/json") + do ..CreateSSLConfigIfNonExistent("GitExtensionForIris") + set request.SSLConfiguration = "GitExtensionForIris" set sc = request.Get(urlComponents("path")) if sc '= $$$OK { @@ -98,6 +119,34 @@ Method Exchange(authCode As %String, verifier As %String, Output sc As %Status) } } +ClassMethod CreateSSLConfigIfNonExistent(name As %String) +{ + 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) +} + ClassMethod GetURLWithParams(url As %String, ByRef params As %String) As %String { if $find(url, "?") { @@ -116,6 +165,7 @@ ClassMethod GetURLWithParams(url As %String, ByRef params As %String) As %String quit:(isLastIter) + // TODO-etamarch -> I had issues with $$$URLENCODE, need to look into this later set url = url_$$$URLENCODE(curParamKey)_"="_$$$URLENCODE(curParamValue) } return url @@ -142,35 +192,22 @@ Storage Default Scopes - - - -Name - - -ClientID + +Username - -ClientSecret + +state - -Endpoint - - -RedirectURL - - -Scopes + +verifier ^SourceControl.Git.O7826.ConfigD ConfigDefaultData ^SourceControl.Git.O7826.ConfigD ^SourceControl.Git.O7826.ConfigI -ConfigState ^SourceControl.Git.O7826.ConfigS -%Storage.Serial +%Storage.Persistent } } - diff --git a/cls/SourceControl/Git/OAuth2/Endpoint.cls b/cls/SourceControl/Git/OAuth2/Endpoint.cls index 599f14fe..a491fda4 100644 --- a/cls/SourceControl/Git/OAuth2/Endpoint.cls +++ b/cls/SourceControl/Git/OAuth2/Endpoint.cls @@ -28,4 +28,3 @@ Storage Default } } - diff --git a/cls/SourceControl/Git/Settings.cls b/cls/SourceControl/Git/Settings.cls index 0761bbe0..32571dc1 100644 --- a/cls/SourceControl/Git/Settings.cls +++ b/cls/SourceControl/Git/Settings.cls @@ -159,6 +159,8 @@ 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 if ..basicMode = "system" { kill @storage@("settings", "user", $username, "basicMode") } else { @@ -441,15 +443,17 @@ Method OnAfterConfigure() As %Boolean $$$ThrowOnError(workMgr.Queue("##class(SourceControl.Git.Utils).Init")) $$$ThrowOnError(workMgr.WaitForComplete()) } elseif (value = 2) { - set remoteTypes("1") = "HTTPS (Only remotes with OAuth2 support)" - set remoteTypes("2") = "SSH" - set response = ##class(%Library.Prompt).GetString("Git remote type:",.remoteType,.remoteTypes,,,defaultPromptFlag + $$$InitialDisplayMask) + set remoteTypes(1) = "HTTPS (Only remotes with OAuth2 support)" + set remoteTypes(2) = "SSH" + set remoteValue = "" + set response = ##class(%Library.Prompt).GetMenu("Git remote type:",.remoteValue,.remoteTypes,,defaultPromptFlag + $$$InitialDisplayMask) if (response '= $$$SuccessResponse) { quit } - set ..gitRemoteType = $select((remoteType = "1"): "HTTPS", 1: "SSH") + set ..gitRemoteType = $select((remoteValue = 1): "HTTPS", 1: "SSH") + set remote = "" set response = ##class(%Library.Prompt).GetString("Git remote URL:",.remote,,,,defaultPromptFlag) if (response '= $$$SuccessResponse) { quit @@ -457,12 +461,17 @@ Method OnAfterConfigure() As %Boolean if (remote = "") { quit } + set ..gitRemoteURL = remote + do ..%Save() if ..gitRemoteType = "HTTPS" { - do ##class(OAuth2).Configure() - set redirectURL = ##class(OAuth2).GetOAuthRedirectEndpoint()_"/"_$NAMESPACE - Write "Please navigate to "_" on your browser to login to the git server", ! - + /* Removed for testing + set c = ##class(SourceControl.Git.OAuth2).Configure(remote) + set redirectURL = ##class(SourceControl.Git.OAuth2).GetOAuthRedirectEndpoint()_"/"_$NAMESPACE + set authURL = ##class(SourceControl.Git.OAuth2).AuthCodeURL(c,$namespace, .state, .verifier) + Write !, "Please navigate to following link on your browser to login to the git server" + write !, authURL + */ // poll attempt count set try = 0 @@ -471,7 +480,7 @@ Method OnAfterConfigure() As %Boolean // stop polling after `TIMEOUT` seconds set TIMEOUT = 300 While try*SLEEPTIME < TIMEOUT { - do ##class(SourceControl.Git.Util.CredentialManager).GetToken(, .err, .code) + 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 diff --git a/cls/SourceControl/Git/Util/CredentialManager.cls b/cls/SourceControl/Git/Util/CredentialManager.cls index 825076fd..28560bcc 100644 --- a/cls/SourceControl/Git/Util/CredentialManager.cls +++ b/cls/SourceControl/Git/Util/CredentialManager.cls @@ -166,7 +166,8 @@ ClassMethod Start() [ Private ] } } -ClassMethod StartInternal() [ Private ] +/// TODO-etamarch -> This was Private, but we cannot have jobs call private methods. Need to find a structural workaround +ClassMethod StartInternal() { try { set lock = $System.AutoLock.Lock(..GetEventName(), , 2) diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index 75b1053b..efaf777d 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -82,6 +82,16 @@ ClassMethod DecomposeProdAllowIDE() As %Boolean [ CodeMode = expression ] $Get(@..#Storage@("settings","decomposeProdAllowIDE"), 0) } +ClassMethod GitRemoteType() As %Boolean [ CodeMode = expression ] +{ +$Get(@..#Storage@("settings","gitRemoteType"), 0) +} + +ClassMethod GitRemoteURL() As %Boolean [ CodeMode = expression ] +{ +$Get(@..#Storage@("settings","gitRemoteURL"), 0) +} + ClassMethod FavoriteNamespaces() As %String { set favNamespaces = [] @@ -264,7 +274,7 @@ ClassMethod UserAction(InternalName As %String, MenuName As %String, ByRef Targe 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/"_$namespace_"/?"_urlPostfix + set Target = urlPrefix _ "/isc/studio/usertemplates/gitsourcecontrol/oauth2.csp/"_$namespace _"/?"_urlPostfix } elseif (menuItemName = "Export") || (menuItemName = "ExportForce") { write !, "==export start==",! set ec = ..ExportAll($case(menuItemName="ExportForce",1:$$$Force,:0)) @@ -664,6 +674,11 @@ ClassMethod Pull(remote As %String = "origin", pTerminateOnError As %Boolean = 0 ClassMethod Clone(remote As %String) As %Status { set settings = ##class(SourceControl.Git.Settings).%New() + // Modify remote if using https + if (remote [ "https") { + set token = ##class(SourceControl.Git.Util.CredentialManager).GetToken($username, .err, .code) + set remote = ##class(SourceControl.Git.OAuth2).GetRemoteURLWithToken(token) + } // TODO: eventually use /ENV flag with GIT_TERMINAL_PROMPT=0. (This isn't doc'd yet and is only in really new versions.) set sc = ..RunGitWithArgs(.errStream, .outStream, "clone", remote, settings.namespaceTemp) // can I substitute this with the new print method? @@ -1861,6 +1876,13 @@ ClassMethod RunGitCommandWithInput(command As %String, inFile As %String = "", O set newArgs($increment(newArgs)) = command + if ((..GitRemoteType() = "HTTPS") && ((command = "pull") || (command = "push") || (command = "fetch"))) { + // Need to use token + set token = ##class(SourceControl.Git.Util.CredentialManager).GetToken($username,.err,.code) + set url = ##class(SourceControl.Git.OAuth2).GetRemoteURLWithToken(token) + set newArgs($increment(newArgs)) = url + } + set syncIrisWithDiff = 0 // whether IRIS needs to be synced with repo file changes using diff output set syncIrisWithCommand = 0 // // whether IRIS needs to be synced with repo file changes using command output set diffBase = "" diff --git a/csp/oauth2.csp b/csp/oauth2.csp index ff50858a..3c07f8b3 100644 --- a/csp/oauth2.csp +++ b/csp/oauth2.csp @@ -1,34 +1,58 @@ - +.alert { + padding: 20px; + background-color: #04AA6D;; + color: white; +} + +.closebtn { + margin-left: 15px; + color: white; + font-weight: bold; + float: right; + font-size: 22px; + line-height: 20px; + cursor: pointer; + transition: 0.3s; +} + +.closebtn:hover { + color: black; +} + + + + 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 - if '$Data(%session.Data("state"))#2 { - w "1 sessId: "_%session.SessionId - w "
"_$Get(%session.Data("state")) - quit 1 - } elseif (%session.Data("state") '= state){ - w "2" + set config = ##class(SourceControl.Git.OAuth2.Config).GetConfig($username) + if (config.state '= state){ + w "Invalid State" quit 1 } @@ -37,29 +61,134 @@ quit 1 } - set verifier = $select($Data(%request.Data("verifier",1),verifier)#2: verifier, 1: "") - - // switch to the namespace that the extension is installed to - set namespace = $Piece(state,"_",1) - new $Namespace - set $Namespace = namespace - - set result = ##class(SourceControl.Git.OAuth2).ExchangeForGithub(code, verifier, .sc) + set verifier = config.verifier + + set result = config.Exchange(code, verifier, .sc) if sc '= $$$OK { do SYSTEM.Status.DisplayError(sc) w "
" w "Unable to retreive access token" } else { - do ##class(SourceControl.Git.Util.CredentialManager).SetToken(, .err, .code) + do ##class(SourceControl.Git.Util.CredentialManager).SetToken($username,result, .err, .code) if (code '= 1) || (err '= "") { w "Unable to save credential" } else { - w "configured!" + set authenticated = 1 } } // https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=GNET_http - + } -
\ No newline at end of file + set namespace = $NAMESPACE + set username = $USERNAME + set config = ##class(SourceControl.Git.OAuth2.Config).GetConfig(username) + 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 = ##class(SourceControl.Git.OAuth2).GetOAuthRedirectEndpoint() + + 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 + } + + do config.%Save() + + set authCodeURL = ##class(SourceControl.Git.OAuth2).AuthCodeURL(config,namespace,.state,.verifier) + set config.state = state + set config.verifier = verifier + do config.%Save() + } + +
+ + +
+ × + Success! You have been authenticated. +
+
+
+ +
+ +
+ + set clientID = $select(config:config.ClientID, 1: "") + + +
+
+ +
+ +
+ + set clientSecret = $select(config:config.ClientSecret, 1: "") + + +
+
+ +
+ +
+ + set authURL = $select(config:config.Endpoint.AuthURL, 1: "") + + +
+
+ +
+ +
+ + set tokenURL = $select(config:config.Endpoint.TokenURL, 1: "") + + +
+
+ +
+
+ + +
+
+ +
+ + Click Here + + + +
+ + + + + + + \ No newline at end of file From e73827d886b885ef76963d11657c90472e6c8056 Mon Sep 17 00:00:00 2001 From: Elijah Tamarchenko Date: Tue, 18 Mar 2025 13:55:04 -0400 Subject: [PATCH 14/27] Working prototype --- cls/SourceControl/Git/OAuth2.cls | 36 -------------- cls/SourceControl/Git/OAuth2/Config.cls | 45 ++++++++++++++--- cls/SourceControl/Git/Settings.cls | 39 +++++++-------- .../Git/Util/CredentialManager.cls | 26 +++++++++- cls/SourceControl/Git/Utils.cls | 24 ++++++++-- cls/SourceControl/Git/WebUIDriver.cls | 13 +++++ csp/oauth2.csp | 38 ++++++++++++++- .../share/git-webui/webui/css/git-webui.css | 16 +++++++ .../share/git-webui/webui/img/oauth.svg | 2 + .../share/git-webui/webui/js/git-webui.js | 48 ++++++++++++------- .../share/git-webui/webui/css/git-webui.less | 20 ++++++++ .../src/share/git-webui/webui/img/oauth.svg | 2 + .../src/share/git-webui/webui/js/git-webui.js | 48 ++++++++++++------- 13 files changed, 253 insertions(+), 104 deletions(-) create mode 100644 git-webui/release/share/git-webui/webui/img/oauth.svg create mode 100644 git-webui/src/share/git-webui/webui/img/oauth.svg diff --git a/cls/SourceControl/Git/OAuth2.cls b/cls/SourceControl/Git/OAuth2.cls index 56527e7e..8024a34f 100644 --- a/cls/SourceControl/Git/OAuth2.cls +++ b/cls/SourceControl/Git/OAuth2.cls @@ -13,42 +13,6 @@ ClassMethod GenerateVerifier() As %String return ##class(%SYSTEM.Encryption).GenCryptRand(32) } -/// Gets the AuthCodeURL for github -/// namespace: is the name of the namespace that git source control is installed in -/// (we need this to ensure that we switch to the right namespace when we get the authcode from the server) -/// state -- Output : random 32 byte string to validate authCode redirects -/// verifier -- Output : random 32 byte string to validate during token exchange -ClassMethod AuthCodeURLForGithub(namespace As %String, Output state, Output verifier) As %String -{ - // (clientID As %String, clientSecret As %String, authEndpoint As %String, tokenEndpoint As %String, redirectURL As %String, scopes As %String) As %Status - set config = ##class(SourceControl.Git.OAuth2.Config).%New( - "Github", - "Ov23lic1u2IDhK6R5m1Y", - "2a84f4846510359697bf0b5f3288330c6a7fe995", - "https://github.com/login/oauth/authorize", - "https://github.com/login/oauth/access_token" , - ..GetOAuthRedirectEndpoint(), - $lb("repo")) - - return ..AuthCodeURL(config, namespace, .state, .verifier) -} - -/// Exchanges an authorization code and associated verifier with the server and returns the retreived access_token -/// authCode: Authorization code received from github after user authenticates -/// verifier: PKCE verifier whose hash was sent to github via the authCodeURL -ClassMethod ExchangeForGithub(authCode As %String, verifier As %String, Output sc As %Status) As %String -{ - set config = ##class(SourceControl.Git.OAuth2.Config).%New( - "Github", - "Ov23lic1u2IDhK6R5m1Y", - "2a84f4846510359697bf0b5f3288330c6a7fe995", - "https://github.com/login/oauth/authorize", - "https://github.com/login/oauth/access_token", - ..GetOAuthRedirectEndpoint(), - $lb("repo")) - return config.Exchange(authCode, verifier, .sc) -} - /// 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 { diff --git a/cls/SourceControl/Git/OAuth2/Config.cls b/cls/SourceControl/Git/OAuth2/Config.cls index 9cb1ccd1..92c2a8e3 100644 --- a/cls/SourceControl/Git/OAuth2/Config.cls +++ b/cls/SourceControl/Git/OAuth2/Config.cls @@ -4,11 +4,11 @@ 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 -Property ClientID As %String(MAXLEN = ""); +/// 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 -Property ClientSecret As %String(MAXLEN = ""); +/// 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; @@ -26,9 +26,37 @@ Property Scopes As %List; Property Username As %String; -Index NameID On Name [ IdKey ]; +Index Username On Username [ IdKey, Unique ]; -Index Username On Username [ 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 { @@ -42,6 +70,7 @@ ClassMethod GetConfig(username As %String) As SourceControl.Git.OAuth2.Config 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() @@ -49,6 +78,7 @@ Method %OnNew(configName As %String, clientID As %String, clientSecret As %Strin set ..Endpoint.TokenURL = tokenEndpoint set ..RedirectURL = redirectURL + if ('scopes) { set scopes = $lb("repo") } @@ -201,6 +231,9 @@ Storage Default verifier + +Name + ^SourceControl.Git.O7826.ConfigD ConfigDefaultData diff --git a/cls/SourceControl/Git/Settings.cls b/cls/SourceControl/Git/Settings.cls index 32571dc1..4d05ece6 100644 --- a/cls/SourceControl/Git/Settings.cls +++ b/cls/SourceControl/Git/Settings.cls @@ -35,11 +35,11 @@ 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()} ]; -/// Type of git remote -Property gitRemoteType As %String(VALUELIST = ",HTTPS,SSH"); - /// URL for git remote -Property gitRemoteURL As %String(MAXLEN = ""); +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.Utils).GitRemoteType()} ]; /// 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()} ]; @@ -307,6 +307,8 @@ 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 inst.gitRemoteType = inst.GetRemoteType(inst.gitRemoteURL)}") } else { do %code.WriteLine(" set response = ##class(%Library.Prompt).GetString("_promptQuoted_",.value,,,,"_defaultPromptFlag_")") } @@ -443,17 +445,7 @@ Method OnAfterConfigure() As %Boolean $$$ThrowOnError(workMgr.Queue("##class(SourceControl.Git.Utils).Init")) $$$ThrowOnError(workMgr.WaitForComplete()) } elseif (value = 2) { - set remoteTypes(1) = "HTTPS (Only remotes with OAuth2 support)" - set remoteTypes(2) = "SSH" - set remoteValue = "" - set response = ##class(%Library.Prompt).GetMenu("Git remote type:",.remoteValue,.remoteTypes,,defaultPromptFlag + $$$InitialDisplayMask) - if (response '= $$$SuccessResponse) { - quit - } - - set ..gitRemoteType = $select((remoteValue = 1): "HTTPS", 1: "SSH") - - set remote = "" + set remote = $select(..gitRemoteURL:..gitRemoteURL, 1:"") set response = ##class(%Library.Prompt).GetString("Git remote URL:",.remote,,,,defaultPromptFlag) if (response '= $$$SuccessResponse) { quit @@ -462,16 +454,12 @@ Method OnAfterConfigure() As %Boolean quit } set ..gitRemoteURL = remote + set ..gitRemoteType = ..GetRemoteType(..gitRemoteURL) do ..%Save() if ..gitRemoteType = "HTTPS" { - /* Removed for testing - set c = ##class(SourceControl.Git.OAuth2).Configure(remote) - set redirectURL = ##class(SourceControl.Git.OAuth2).GetOAuthRedirectEndpoint()_"/"_$NAMESPACE - set authURL = ##class(SourceControl.Git.OAuth2).AuthCodeURL(c,$namespace, .state, .verifier) - Write !, "Please navigate to following link on your browser to login to the git server" - write !, authURL - */ + 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" // poll attempt count set try = 0 @@ -509,6 +497,8 @@ Method OnAfterConfigure() As %Boolean $$$ThrowOnError(##class(SourceControl.Git.Utils).AddToSourceControl(##class(SourceControl.Git.Settings.Document).#INTERNALNAME)) $$$ThrowOnError(##class(SourceControl.Git.Utils).Commit(##class(SourceControl.Git.Settings.Document).#INTERNALNAME,"initial commit")) } + // Set remote to remove token + do ##class(SourceControl.Git.Utils).SetConfiguredRemote(remote) } } } @@ -566,4 +556,9 @@ Method SaveDefaults() As %Boolean return ##class(%zpkg.isc.sc.git.Defaults).SetDefaultSettings(defaults) } +ClassMethod GetRemoteType(remoteURL 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 index 28560bcc..388970aa 100644 --- a/cls/SourceControl/Git/Util/CredentialManager.cls +++ b/cls/SourceControl/Git/Util/CredentialManager.cls @@ -61,7 +61,7 @@ ClassMethod GetToken(gitUsername As %String = "", Output error As %String, Outpu return token } -/// SetToken is used to retreive the access token for a particular git user +/// 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) @@ -77,6 +77,30 @@ ClassMethod SendResponse(toPID As %Integer, message As %String, error As %String } } +/// 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 { diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index efaf777d..08d5101e 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -82,12 +82,12 @@ ClassMethod DecomposeProdAllowIDE() As %Boolean [ CodeMode = expression ] $Get(@..#Storage@("settings","decomposeProdAllowIDE"), 0) } -ClassMethod GitRemoteType() As %Boolean [ CodeMode = expression ] +ClassMethod GitRemoteType() As %String [ CodeMode = expression ] { $Get(@..#Storage@("settings","gitRemoteType"), 0) } -ClassMethod GitRemoteURL() As %Boolean [ CodeMode = expression ] +ClassMethod GitRemoteURL() As %String [ CodeMode = expression ] { $Get(@..#Storage@("settings","gitRemoteURL"), 0) } @@ -274,7 +274,7 @@ ClassMethod UserAction(InternalName As %String, MenuName As %String, ByRef Targe 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/"_$namespace _"/?"_urlPostfix + 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)) @@ -3212,4 +3212,22 @@ ClassMethod WriteLineToFile(filePath As %String, line As %String) Do file.WriteLine(line) } +ClassMethod Authenticated() As %Boolean +{ + if (##class(SourceControl.Git.Utils).GitRemoteType() '= "SSH") { + 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 +} + } 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/csp/oauth2.csp b/csp/oauth2.csp index 3c07f8b3..82131ddb 100644 --- a/csp/oauth2.csp +++ b/csp/oauth2.csp @@ -16,6 +16,7 @@ body { line-height: 1.5; color: #212529; text-align: left; + padding-top: 20px; } .alert { @@ -114,14 +115,25 @@ body { }
-
× Success! You have been authenticated.
+
+
+

+ 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:

+
@@ -172,8 +184,9 @@ body { - Click Here + +
@@ -189,6 +202,27 @@ body { var form = document.getElementById('oauthForm'); form.submit(); } + + /* HACK: + Popups don't work with redirection (Cross-browser origin problems), + so we want to reopen this in a browser tab as soon as popup is opened + TODO: Need to test opening this from VSCode / Studio (>_<) + */ + var link = document.getElementById('githubLink'); + if (link) { + window.location.href = link.href + } + + // HACK: Zen popups don't allow + if ((window !== window.parent) || (navigator.userAgent.indexOf('MSIE 7') > -1) || (navigator.userAgent.indexOf(" Code/") > -1)) { + var newPage = document.getElementById('pageLink'); + newPage.href = window.location.href; + newPage.click(); + // hack to get the popup window to close + var top = window.self; + top.opener = window.self; + top.close(); + } \ No newline at end of file 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 87b3926d..4332d593 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 = $( '