11package AzureDevopsClient
22
33import (
4+ "encoding/json"
45 "errors"
56 "fmt"
67 "net/url"
@@ -21,7 +22,17 @@ type AzureDevopsClient struct {
2122
2223 organization * string
2324 collection * string
24- accessToken * string
25+
26+ // we can either use a PAT token for authentication ...
27+ accessToken * string
28+
29+ // ... or client id and secret
30+ tenantId * string
31+ clientId * string
32+ clientSecret * string
33+
34+ entraIdToken * EntraIdToken
35+ entraIdTokenLastRefreshed int64
2536
2637 HostUrl * string
2738
@@ -48,6 +59,13 @@ type AzureDevopsClient struct {
4859 }
4960}
5061
62+ type EntraIdToken struct {
63+ token_type * string
64+ expires_in * int64
65+ ext_expires_in * int64
66+ access_token * string
67+ }
68+
5169func NewAzureDevopsClient () * AzureDevopsClient {
5270 c := AzureDevopsClient {}
5371 c .Init ()
@@ -62,6 +80,8 @@ func (c *AzureDevopsClient) Init() {
6280 c .SetRetries (3 )
6381 c .SetConcurrency (10 )
6482
83+ c .entraIdTokenLastRefreshed = 0
84+
6585 c .LimitBuildsPerProject = 100
6686 c .LimitBuildsPerDefinition = 10
6787 c .LimitReleasesPerDefinition = 100
@@ -115,46 +135,131 @@ func (c *AzureDevopsClient) SetAccessToken(token string) {
115135 c .accessToken = & token
116136}
117137
138+ func (c * AzureDevopsClient ) SetTenantId (tenantId string ) {
139+ c .tenantId = & tenantId
140+ }
141+
142+ func (c * AzureDevopsClient ) SetClientId (clientId string ) {
143+ c .clientId = & clientId
144+ }
145+
146+ func (c * AzureDevopsClient ) SetClientSecret (clientSecret string ) {
147+ c .clientSecret = & clientSecret
148+ }
149+
150+ func (c * AzureDevopsClient ) SupportsPatAuthentication () bool {
151+ return c .accessToken != nil
152+ }
153+
154+ func (c * AzureDevopsClient ) SupportsServicePrincipalAuthentication () bool {
155+ return c .tenantId != nil && c .clientId != nil && c .clientSecret != nil
156+ }
157+
158+ func (c * AzureDevopsClient ) HasExpiredEntraIdAccessToken () bool {
159+ var currentUnix = time .Now ().Unix ()
160+
161+ // subtract 60 seconds of offset (should be enough time to use fire all requests)
162+ return (c .entraIdToken == nil || currentUnix >= c .entraIdTokenLastRefreshed + * c .entraIdToken .expires_in - 60 )
163+ }
164+
165+ func (c * AzureDevopsClient ) RefreshEntraIdAccessToken () (string , error ) {
166+ var restClient = resty .New ()
167+
168+ restClient .SetBaseURL (fmt .Sprintf ("https://login.microsoftonline.com/%v/oauth2/v2.0/token" , * c .tenantId ))
169+
170+ restClient .SetFormData (map [string ]string {
171+ "client_id" : * c .clientId ,
172+ "client_secret" : * c .clientSecret ,
173+ "grant_type" : "client_credentials" ,
174+ "scope" : "499b84ac-1321-427f-aa17-267ca6975798" , // the scope is always the same for Azure DevOps
175+ })
176+
177+ restClient .SetHeader ("Content-Type" , "application/x-www-form-urlencoded" )
178+ restClient .SetHeader ("Accept" , "application/json" )
179+ restClient .SetRetryCount (c .RequestRetries )
180+
181+ var response , err = restClient .R ().Post ("" )
182+
183+ if err != nil {
184+ return "" , err
185+ }
186+
187+ err = json .Unmarshal (response .Body (), & c .entraIdToken )
188+
189+ if err != nil {
190+ return "" , err
191+ }
192+
193+ c .entraIdTokenLastRefreshed = time .Now ().Unix ()
194+
195+ return * c .entraIdToken .access_token , nil
196+ }
197+
118198func (c * AzureDevopsClient ) rest () * resty.Client {
199+ var client , err = c .restWithAuthentication ("dev.azure.com" )
200+
201+ if err != nil {
202+ // TODO handle error!
203+ }
204+
205+ return client
206+ }
207+
208+ func (c * AzureDevopsClient ) restVsrm () * resty.Client {
209+ var client , err = c .restWithAuthentication ("vsrm.dev.azure.com" )
210+
211+ if err != nil {
212+ // TODO handle error!
213+ }
214+
215+ return client
216+ }
217+
218+ func (c * AzureDevopsClient ) restWithAuthentication (domain string ) (* resty.Client , error ) {
119219 if c .restClient == nil {
120- c .restClient = resty .New ()
121- if c .HostUrl != nil {
122- c .restClient .SetBaseURL (* c .HostUrl + "/" + * c .organization + "/" )
123- } else {
124- c .restClient .SetBaseURL (fmt .Sprintf ("https://dev.azure.com/%v/" , * c .organization ))
125- }
126- c .restClient .SetHeader ("Accept" , "application/json" )
220+ c .restClient = c .restWithoutToken (domain )
221+ }
222+
223+ if c .SupportsPatAuthentication () {
127224 c .restClient .SetBasicAuth ("" , * c .accessToken )
128- c .restClient .SetRetryCount (c .RequestRetries )
129- if c .delayUntil != nil {
130- c .restClient .OnBeforeRequest (c .restOnBeforeRequestDelay )
131- } else {
132- c .restClient .OnBeforeRequest (c .restOnBeforeRequest )
133- }
225+ } else if c .SupportsServicePrincipalAuthentication () {
226+ if c .HasExpiredEntraIdAccessToken () {
227+ var accessToken , err = c .RefreshEntraIdAccessToken ()
134228
135- c .restClient .OnAfterResponse (c .restOnAfterResponse )
229+ if err != nil {
230+ return nil , err
231+ }
136232
233+ c .restClient .SetBasicAuth ("" , accessToken )
234+ }
235+ } else {
236+ return nil , errors .New ("no valid authentication method provided" )
137237 }
138238
139- return c .restClient
239+ return c .restClient , nil
140240}
141241
142- func (c * AzureDevopsClient ) restVsrm () * resty.Client {
143- if c .restClientVsrm == nil {
144- c .restClientVsrm = resty .New ()
145- if c .HostUrl != nil {
146- c .restClientVsrm .SetBaseURL (* c .HostUrl + "/" + * c .organization + "/" )
147- } else {
148- c .restClientVsrm .SetBaseURL (fmt .Sprintf ("https://vsrm.dev.azure.com/%v/" , * c .organization ))
149- }
150- c .restClientVsrm .SetHeader ("Accept" , "application/json" )
151- c .restClientVsrm .SetBasicAuth ("" , * c .accessToken )
152- c .restClientVsrm .SetRetryCount (c .RequestRetries )
153- c .restClientVsrm .OnBeforeRequest (c .restOnBeforeRequest )
154- c .restClientVsrm .OnAfterResponse (c .restOnAfterResponse )
242+ func (c * AzureDevopsClient ) restWithoutToken (domain string ) * resty.Client {
243+ var restClient = resty .New ()
244+
245+ if c .HostUrl != nil {
246+ restClient .SetBaseURL (* c .HostUrl + "/" + * c .organization + "/" )
247+ } else {
248+ restClient .SetBaseURL (fmt .Sprintf ("https://%v/%v/" , domain , * c .organization ))
249+ }
250+
251+ restClient .SetHeader ("Accept" , "application/json" )
252+ restClient .SetRetryCount (c .RequestRetries )
253+
254+ if c .delayUntil != nil {
255+ restClient .OnBeforeRequest (c .restOnBeforeRequestDelay )
256+ } else {
257+ restClient .OnBeforeRequest (c .restOnBeforeRequest )
155258 }
156259
157- return c .restClientVsrm
260+ restClient .OnAfterResponse (c .restOnAfterResponse )
261+
262+ return restClient
158263}
159264
160265func (c * AzureDevopsClient ) concurrencyLock () {
0 commit comments