Skip to content

Commit 7c19255

Browse files
authored
Merge pull request #3 from XenitAB/docs
Update documentation
2 parents 771e866 + 790eba6 commit 7c19255

File tree

7 files changed

+176
-2
lines changed

7 files changed

+176
-2
lines changed

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2020 Xenit AB
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
TAG = latest
22
IMG ?= quay.io/xenitab/azdo-proxy:$(TAG)
33

4+
assets:
5+
draw.io -b 10 -x -f png -p 0 -o assets/architecture.png assets/diagram.drawio
6+
.PHONY: assets
7+
48
fmt:
59
go fmt ./...
610

README.md

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,120 @@
11
# Azure DevOps Proxy
2-
Proxy application to allow one PAT to be shared with limited scope to other clients.
2+
[![Go Report Card](https://goreportcard.com/badge/github.com/XenitAB/azdo-proxy)](https://goreportcard.com/report/github.com/XenitAB/azdo-proxy)
3+
[![Docker Repository on Quay](https://quay.io/repository/xenitab/azdo-proxy/status "Docker Repository on Quay")](https://quay.io/repository/xenitab/azdo-proxy)
4+
5+
Proxy to allow controlled sharing of a Azure DevOps Personal Access Token.
6+
7+
Azure DevOps allows the use of Personal Access Tokens (PAT) to authenticate access to both its
8+
API and Git repositories. Sadly it does not provide an API to create new PAT, making the process
9+
of automation cumbersome if multiple tokens are needed with limited scopes.
10+
11+
<p align="center">
12+
<img src="./assets/architecture.png">
13+
</p>
14+
15+
Azure Devops Proxy (azdo-proxy) is an attempt to solve this issue by enabling a single PAT
16+
to be shared by many applications, while at the same time limiting access for each application.
17+
Requests are sent to azdo-proxy together with a token, which gives access to a specific repository.
18+
The request is checked and if allowed forwarded to Azure DevOps with the PAT appended to the request.
19+
20+
## How To
21+
Start off by [creating a new PAT](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=preview-page) as it has to be given to the proxy.
22+
23+
> The example will show how to run azdo-proxy in Kubernetes, but there is nothing limiting azdo-proxy to run in any other environment.
24+
25+
The proxy reads its configuration from a JSON file. The file will contain the PAT used to authenticate requests with, the Azure DevOps organization, and a list of repositories that can be accessed through the proxy along with a unique token for each repository.
26+
```json
27+
{
28+
"pat": "<pat>",
29+
"organization": "org",
30+
"repositories": [
31+
{
32+
"project": "project",
33+
"name": "repo-1",
34+
"token": "<token-1>"
35+
},
36+
{
37+
"project": "project",
38+
"name": "repo-2",
39+
"token": "<token-2>"
40+
}
41+
]
42+
}
43+
```
44+
45+
Create a Kubernetes secret containing the configuration JSON file.
46+
```shell
47+
kubectl create secret generic azdo-proxy-config --from-file=config.json
48+
```
49+
50+
Add the Helm repository and install the chart, be sure to set the secret name.
51+
```shell
52+
helm repo add https://xenitab.github.io/azdo-proxy/
53+
helm install azdo-proxy --set configSecretName=azdo-proxy-config
54+
```
55+
56+
There should now be a azdo-proxy Pod and Service in the cluster, ready to proxy traffic.
57+
58+
### GIT
59+
Cloning a repository through the proxy is not too different from doing so directly from Azure DevOps.
60+
The only limitation is that it is not possible to clone through ssh, as azdo-proxy only proxies http traffic.
61+
To clone the repository `repo-1` [get the clone url from the respository page](https://docs.microsoft.com/en-us/azure/devops/repos/git/clone?view=azure-devops&tabs=visual-studio#get-the-clone-url-to-your-repo).
62+
Then replace the host part of the url with `azdo-proxy` and att the token as a basci auth parameter.
63+
The result should be similar to below.
64+
```shell
65+
git clone http://<token-1>@azdo-proxy/org/proj/_git/repo-1
66+
```
67+
68+
### API
69+
Authenticated API calls can also be done through the proxy. Currently only repository specific
70+
requests will be permitted. This may change in future releases. As an example execute the
71+
following command to list all pull requests in the repository `repo-1`.
72+
```shell
73+
curl http://<token-1>@azdo-proxy/org/proj/_apis/git/repositories/repo-1/pullrequests?api-version=5.1
74+
```
75+
76+
> :warning: **If you intend on using a language specific API**: Please read this!
77+
78+
Some APIs built by Microsoft, like [azure-devops-go-api](https://github.com/microsoft/azure-devops-go-api), will make a request to the [Resource Areas API](https://docs.microsoft.com/en-us/azure/devops/extend/develop/work-with-urls?view=azure-devops&tabs=http#how-to-get-an-organizations-url)
79+
which returns a list of location URLs for a specific organization. They will then use those URLs
80+
when making additional requests, skipping the proxy. To avoid this you need to explicitly create
81+
your client instead of allowing it to be created automatically.
82+
83+
In the case of Go you should create a client in the following way.
84+
```golang
85+
package main
86+
87+
import (
88+
"github.com/microsoft/azure-devops-go-api/azuredevops"
89+
"github.com/microsoft/azure-devops-go-api/azuredevops/git"
90+
)
91+
92+
func main() {
93+
connection := azuredevops.NewAnonymousConnection("http://azdo-proxy")
94+
client := connection.GetClientByUrl("http://azdo-proxy")
95+
gitClient := &git.ClientImpl{
96+
Client: *client,
97+
}
98+
}
99+
```
100+
101+
Instead of the cleaner solution which would ignore the proxy.
102+
```golang
103+
package main
104+
105+
import (
106+
"context"
107+
108+
"github.com/microsoft/azure-devops-go-api/azuredevops"
109+
"github.com/microsoft/azure-devops-go-api/azuredevops/git"
110+
)
111+
112+
func main() {
113+
connection := azuredevops.NewAnonymousConnection("http://azdo-proxy")
114+
ctx := context.Background()
115+
gitClient, _ := git.NewClient(ctx, connection)
116+
}
117+
```
118+
119+
## License
120+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

assets/architecture.png

9.07 KB
Loading

assets/diagram.drawio

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<mxfile host="Electron" modified="2020-07-09T12:44:40.518Z" agent="5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/13.3.5 Chrome/83.0.4103.119 Electron/9.0.5 Safari/537.36" etag="ny_sSromxSdBE-dT1N06" version="13.3.5" type="device"><diagram id="gbA4HWZm-8l8qiuZn7tg" name="architecture">1ZfbjpswEIafJpepOASavdyEdHtUW6VS26vKCwO4NQw1JkCevg6YAPIm3a66OVyB/xlj++OfQUzsZVLdcZLFHzAANrGMoJrY3sSyLMO15GWn1K1iWjOjVSJOA6X1wppuQYldWkEDyEeJApEJmo1FH9MUfDHSCOdYjtNCZONVMxKBJqx9wnT1Kw1E3Kq2bRh94DXQKFZLz9y5mpKQLlul5jEJsBxI9mpiLzmiaO+Saglsh68D0857dSC63xmHVDxqQvbuhhPvzdSyivLzxtu+xd9Tu33KhrBCnfjT7Re1X1F3FCCQUNQQuYgxwpSwVa8uOBZpALuVDDnqc94jZlI0pfgThKjVGyaFQCnFImEqqp9GHTDHgvtw5AidLQiPQBzJU1bcnWWwgGJ1B5iA4LVM4MCIoJuxAYjyUbTP60nLGwX7H8CbGniyDXCacaxqjf+YbhlTAeuMNFRKWXhjkiFlbIkMeTPXDgjMQ1/queD4CwYR15/DfXiM/Qa4gOoorS5qdBWhKn3W2b7sy8bstHhQMa7xTIQtjbDGleRZ2zRCWu3wDjFmSFPR7MlZTBxPKoTRKJWCLzGBZLigSdM9FiGmQhnbtHrdo0kkd87o/W7/uU9AXm+3BYcfHmw+ZvmLfBP9L/y2M8K/b1AD/M4D9J3nov9Soy+k+9LO9wfN/YTWIbHx+pua3wy+DyNeNQx5tRo9veHMHtlwzItqODO94WTZX1/HJfaa+Wzcapz5uVvN/IDZ9Y4z/JKmmJ750+lcpZOdB52ss758J2tfzfNb+eaAle2TWHnfys3TtHL3KgvAfbAA9Dd0BQVguqcrADns/7aa2OCv1V79AQ==</diagram></mxfile>

pkg/config/config.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ type Repository struct {
1616
}
1717

1818
type Configuration struct {
19-
Domain string `json:"domain" validate:"required"`
19+
Domain string `json:"domain,omitempty"`
2020
Pat string `json:"pat" validate:"required"`
2121
Organization string `json:"organization" validate:"required"`
2222
Repositories []Repository `json:"repositories" validate:"required"`
@@ -45,6 +45,10 @@ func LoadConfiguration(src io.Reader) (*Configuration, error) {
4545
return nil, err
4646
}
4747

48+
if len(c.Domain) == 0 {
49+
c.Domain = "dev.azure.com"
50+
}
51+
4852
err = validate.New().Struct(c)
4953
if err != nil {
5054
return nil, err

pkg/config/config_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,20 @@ const missingParamJson = `
4141
}
4242
`
4343

44+
const minimalJson = `
45+
{
46+
"pat": "foobar",
47+
"organization": "xenitab",
48+
"repositories": [
49+
{
50+
"name": "gitops-deployment",
51+
"project": "Lab",
52+
"token": "foobar"
53+
}
54+
]
55+
}
56+
`
57+
4458
func TestValidJson(t *testing.T) {
4559
reader := strings.NewReader(validJson)
4660
_, err := LoadConfiguration(reader)
@@ -64,3 +78,15 @@ func TestMissingParam(t *testing.T) {
6478
t.Error("error should not be nil")
6579
}
6680
}
81+
82+
func TestMinimalJson(t *testing.T) {
83+
reader := strings.NewReader(minimalJson)
84+
c, err := LoadConfiguration(reader)
85+
if err != nil {
86+
t.Errorf("could not parse json: %v", err)
87+
}
88+
89+
if c.Domain != "dev.azure.com" {
90+
t.Errorf("default domain incorrect")
91+
}
92+
}

0 commit comments

Comments
 (0)