Skip to content

Commit

Permalink
Merge pull request #3 from XenitAB/docs
Browse files Browse the repository at this point in the history
Update documentation
  • Loading branch information
phillebaba authored Jul 9, 2020
2 parents 771e866 + 790eba6 commit 7c19255
Show file tree
Hide file tree
Showing 7 changed files with 176 additions and 2 deletions.
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2020 Xenit AB

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
TAG = latest
IMG ?= quay.io/xenitab/azdo-proxy:$(TAG)

assets:
draw.io -b 10 -x -f png -p 0 -o assets/architecture.png assets/diagram.drawio
.PHONY: assets

fmt:
go fmt ./...

Expand Down
120 changes: 119 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,120 @@
# Azure DevOps Proxy
Proxy application to allow one PAT to be shared with limited scope to other clients.
[![Go Report Card](https://goreportcard.com/badge/github.com/XenitAB/azdo-proxy)](https://goreportcard.com/report/github.com/XenitAB/azdo-proxy)
[![Docker Repository on Quay](https://quay.io/repository/xenitab/azdo-proxy/status "Docker Repository on Quay")](https://quay.io/repository/xenitab/azdo-proxy)

Proxy to allow controlled sharing of a Azure DevOps Personal Access Token.

Azure DevOps allows the use of Personal Access Tokens (PAT) to authenticate access to both its
API and Git repositories. Sadly it does not provide an API to create new PAT, making the process
of automation cumbersome if multiple tokens are needed with limited scopes.

<p align="center">
<img src="./assets/architecture.png">
</p>

Azure Devops Proxy (azdo-proxy) is an attempt to solve this issue by enabling a single PAT
to be shared by many applications, while at the same time limiting access for each application.
Requests are sent to azdo-proxy together with a token, which gives access to a specific repository.
The request is checked and if allowed forwarded to Azure DevOps with the PAT appended to the request.

## How To
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.

> The example will show how to run azdo-proxy in Kubernetes, but there is nothing limiting azdo-proxy to run in any other environment.
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.
```json
{
"pat": "<pat>",
"organization": "org",
"repositories": [
{
"project": "project",
"name": "repo-1",
"token": "<token-1>"
},
{
"project": "project",
"name": "repo-2",
"token": "<token-2>"
}
]
}
```

Create a Kubernetes secret containing the configuration JSON file.
```shell
kubectl create secret generic azdo-proxy-config --from-file=config.json
```

Add the Helm repository and install the chart, be sure to set the secret name.
```shell
helm repo add https://xenitab.github.io/azdo-proxy/
helm install azdo-proxy --set configSecretName=azdo-proxy-config
```

There should now be a azdo-proxy Pod and Service in the cluster, ready to proxy traffic.

### GIT
Cloning a repository through the proxy is not too different from doing so directly from Azure DevOps.
The only limitation is that it is not possible to clone through ssh, as azdo-proxy only proxies http traffic.
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).
Then replace the host part of the url with `azdo-proxy` and att the token as a basci auth parameter.
The result should be similar to below.
```shell
git clone http://<token-1>@azdo-proxy/org/proj/_git/repo-1
```

### API
Authenticated API calls can also be done through the proxy. Currently only repository specific
requests will be permitted. This may change in future releases. As an example execute the
following command to list all pull requests in the repository `repo-1`.
```shell
curl http://<token-1>@azdo-proxy/org/proj/_apis/git/repositories/repo-1/pullrequests?api-version=5.1
```

> :warning: **If you intend on using a language specific API**: Please read this!
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)
which returns a list of location URLs for a specific organization. They will then use those URLs
when making additional requests, skipping the proxy. To avoid this you need to explicitly create
your client instead of allowing it to be created automatically.

In the case of Go you should create a client in the following way.
```golang
package main

import (
"github.com/microsoft/azure-devops-go-api/azuredevops"
"github.com/microsoft/azure-devops-go-api/azuredevops/git"
)

func main() {
connection := azuredevops.NewAnonymousConnection("http://azdo-proxy")
client := connection.GetClientByUrl("http://azdo-proxy")
gitClient := &git.ClientImpl{
Client: *client,
}
}
```

Instead of the cleaner solution which would ignore the proxy.
```golang
package main

import (
"context"

"github.com/microsoft/azure-devops-go-api/azuredevops"
"github.com/microsoft/azure-devops-go-api/azuredevops/git"
)

func main() {
connection := azuredevops.NewAnonymousConnection("http://azdo-proxy")
ctx := context.Background()
gitClient, _ := git.NewClient(ctx, connection)
}
```

## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
Binary file added assets/architecture.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions assets/diagram.drawio
Original file line number Diff line number Diff line change
@@ -0,0 +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>
6 changes: 5 additions & 1 deletion pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ type Repository struct {
}

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

if len(c.Domain) == 0 {
c.Domain = "dev.azure.com"
}

err = validate.New().Struct(c)
if err != nil {
return nil, err
Expand Down
26 changes: 26 additions & 0 deletions pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,20 @@ const missingParamJson = `
}
`

const minimalJson = `
{
"pat": "foobar",
"organization": "xenitab",
"repositories": [
{
"name": "gitops-deployment",
"project": "Lab",
"token": "foobar"
}
]
}
`

func TestValidJson(t *testing.T) {
reader := strings.NewReader(validJson)
_, err := LoadConfiguration(reader)
Expand All @@ -64,3 +78,15 @@ func TestMissingParam(t *testing.T) {
t.Error("error should not be nil")
}
}

func TestMinimalJson(t *testing.T) {
reader := strings.NewReader(minimalJson)
c, err := LoadConfiguration(reader)
if err != nil {
t.Errorf("could not parse json: %v", err)
}

if c.Domain != "dev.azure.com" {
t.Errorf("default domain incorrect")
}
}

0 comments on commit 7c19255

Please sign in to comment.