mirror of
https://github.com/docker/docker-credential-helpers.git
synced 2026-06-28 15:21:29 +05:30
Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1057cf7f86 | |||
| 40d06d0090 | |||
| de50f50ab0 | |||
| 3c3e1d3af1 | |||
| 19ec1c3164 | |||
| 2a3f7a4468 | |||
| b7c53e02cd | |||
| 94963d0da8 | |||
| 2514d5e8b2 | |||
| 01ed4b811b | |||
| 9b6be7c243 | |||
| f853612e07 | |||
| 69c9d2eab8 | |||
| eecc09c974 | |||
| 48079a964a | |||
| cfbce1c845 | |||
| a994ca1d54 | |||
| f72c04f1d8 | |||
| c45d9e9e28 | |||
| 80833adff5 | |||
| f1498a0524 | |||
| c2abee4c0c | |||
| 9e96a4905d | |||
| cf9b6df432 | |||
| b0c64357eb | |||
| 5b764cc13a | |||
| b63a32e7a2 | |||
| 173fe2dbc2 | |||
| b3ebaa455d | |||
| 029e094488 | |||
| 8fa18eb16c | |||
| 887a66459a | |||
| d2e6ed779a | |||
| 9557dc3c3f | |||
| 59b3d54595 | |||
| 205e3b3056 | |||
| 7566a1e399 | |||
| 72661b3103 | |||
| 5a8fb214ed | |||
| 5128fa1bad | |||
| fee9277aa7 | |||
| d80b179703 | |||
| c4fc9c07dd | |||
| 2c63e775b4 | |||
| 00703eb6db | |||
| df8c7a02f1 | |||
| 4b8917b1cf | |||
| 039c315f22 | |||
| 6bbb56ae3c | |||
| ec937eebe2 | |||
| fa24bb8912 | |||
| 76055c2deb | |||
| e68b300c17 | |||
| 7d02ef740b | |||
| bcc242e1ad | |||
| 8727ffc77b | |||
| 2275377a31 | |||
| 0fb2225199 | |||
| a96948acb3 |
+16
-3
@@ -1,15 +1,24 @@
|
|||||||
---
|
---
|
||||||
# See appveyor.yml for windows build.
|
# See appveyor.yml for windows build.
|
||||||
sudo: false
|
sudo: required
|
||||||
language: go
|
language: go
|
||||||
|
dist: trusty
|
||||||
os:
|
os:
|
||||||
|
- linux
|
||||||
- osx
|
- osx
|
||||||
notifications:
|
notifications:
|
||||||
email: false
|
email: false
|
||||||
go:
|
go:
|
||||||
- 1.6
|
- 1.6
|
||||||
install: make deps
|
install: make deps
|
||||||
before_script: make validate
|
addons:
|
||||||
|
apt:
|
||||||
|
packages:
|
||||||
|
- libsecret-1-dev
|
||||||
|
before_script:
|
||||||
|
- "export DISPLAY=:99.0"
|
||||||
|
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sh ci/before_script_linux.sh; fi
|
||||||
|
- make validate
|
||||||
script: make test
|
script: make test
|
||||||
|
|
||||||
before_deploy:
|
before_deploy:
|
||||||
@@ -19,7 +28,11 @@
|
|||||||
provider: releases
|
provider: releases
|
||||||
api_key:
|
api_key:
|
||||||
secure: "cGs5cao/MeVQVnum+Pr/Tpv+w83NsqGVS3wxvi3LYEf2ON4Kkmtd+Alwi0YFkGPJmSY0jZOct8NVK/M70qSnIU4l+AAq9+3KSMv23u4xrmy2sQog3AF+Ve3Rac+iYwZHOWwGs9I67CSuVv0vjJNVsDsTVefc25lHJImjRvXIS4p9xYzRPeUDCoqAo/QMVE+vFiMyxydsvt8fhd0gZCjPYWEpyHe9tjZ1tr1HsHZKFAjVb6AmF45d8rvadPoVUuLaOtr35wDC3XRKEvCZUefQpwLkrNj7j2L1rVGlY1xTE2APpLtvfd7R1Mx6kSfS1Gm3Pwcv3mugadXIhecL0lsdnU+BANjX3VUiv4ryzTPbsge966mv9ZQYwAzgCQTWRtMNJqsAnPZTeAkiOntd+HMQbPpxljOxv1sjDPY+EIZesyB3yQRJI8vMxqFcAjxeRyLcBqEnRFC2nd/Ln0KZ7ZFu16FcpNqRojdBayyypuXKqAiBNwtp4ti/65x8eHfBJuNjJtNZkRsJEYam4CYMRLxds9plKQfkaZ8045PKpyXO8fMpUhrfqSVID4IrYvD+io6XoXtdR4Lk6isZ2EgrjdrqgdG70S5lwKihL4iAi2F2ZCWhngFhkeNVOZunEWE6qZMk5wKODajR9sixGDApGPZQVojHwCNRGILZaHZ39JCIj3s="
|
secure: "cGs5cao/MeVQVnum+Pr/Tpv+w83NsqGVS3wxvi3LYEf2ON4Kkmtd+Alwi0YFkGPJmSY0jZOct8NVK/M70qSnIU4l+AAq9+3KSMv23u4xrmy2sQog3AF+Ve3Rac+iYwZHOWwGs9I67CSuVv0vjJNVsDsTVefc25lHJImjRvXIS4p9xYzRPeUDCoqAo/QMVE+vFiMyxydsvt8fhd0gZCjPYWEpyHe9tjZ1tr1HsHZKFAjVb6AmF45d8rvadPoVUuLaOtr35wDC3XRKEvCZUefQpwLkrNj7j2L1rVGlY1xTE2APpLtvfd7R1Mx6kSfS1Gm3Pwcv3mugadXIhecL0lsdnU+BANjX3VUiv4ryzTPbsge966mv9ZQYwAzgCQTWRtMNJqsAnPZTeAkiOntd+HMQbPpxljOxv1sjDPY+EIZesyB3yQRJI8vMxqFcAjxeRyLcBqEnRFC2nd/Ln0KZ7ZFu16FcpNqRojdBayyypuXKqAiBNwtp4ti/65x8eHfBJuNjJtNZkRsJEYam4CYMRLxds9plKQfkaZ8045PKpyXO8fMpUhrfqSVID4IrYvD+io6XoXtdR4Lk6isZ2EgrjdrqgdG70S5lwKihL4iAi2F2ZCWhngFhkeNVOZunEWE6qZMk5wKODajR9sixGDApGPZQVojHwCNRGILZaHZ39JCIj3s="
|
||||||
file: docker-credential-osxkeychain-${TRAVIS_TAG}-amd64.tar.gz
|
# upload file artifacts using a glob expression.
|
||||||
|
# It requires both options `file_glob` and `file`:
|
||||||
|
# https://github.com/travis-ci/dpl/blob/master/lib/dpl/provider/releases.rb#L47-L53
|
||||||
|
file_glob: true
|
||||||
|
file: docker-credential-*-${TRAVIS_TAG}-amd64.tar.gz
|
||||||
# don't delete the artifacts from previous phases
|
# don't delete the artifacts from previous phases
|
||||||
skip_cleanup: true
|
skip_cleanup: true
|
||||||
# deploy when a new tag is pushed
|
# deploy when a new tag is pushed
|
||||||
|
|||||||
@@ -4,6 +4,23 @@ This changelog tracks the releases of docker-credential-helpers.
|
|||||||
This project includes different binaries per platform.
|
This project includes different binaries per platform.
|
||||||
The platform released is identified after the tag name.
|
The platform released is identified after the tag name.
|
||||||
|
|
||||||
|
## v0.4.0 (Go client, Mac OS X, Windows, Linux)
|
||||||
|
|
||||||
|
- Full implementation for OSX ready
|
||||||
|
- Fix some windows issues
|
||||||
|
- Implement client.List, change list API
|
||||||
|
- mac: delete credentials before adding them to avoid already exist error (fixes #37)
|
||||||
|
|
||||||
|
|
||||||
|
## v0.3.0 (Go client)
|
||||||
|
|
||||||
|
- Add Go client library to talk with the native programs.
|
||||||
|
|
||||||
|
## v0.2.0 (Mac OS X, Windows, Linux)
|
||||||
|
|
||||||
|
- Initial release of docker-credential-secretservice for Linux.
|
||||||
|
- Use new secrets payload introduced in https://github.com/docker/docker/pull/20970.
|
||||||
|
|
||||||
## v0.1.0 (Mac OS X, Windows)
|
## v0.1.0 (Mac OS X, Windows)
|
||||||
|
|
||||||
- Initial release of docker-credential-osxkeychain for Mac OS X.
|
- Initial release of docker-credential-osxkeychain for Mac OS X.
|
||||||
|
|||||||
+160
@@ -0,0 +1,160 @@
|
|||||||
|
# docker-credential-helpers maintainers file
|
||||||
|
#
|
||||||
|
# This file describes who runs the docker/docker-credential-helpers project and how.
|
||||||
|
# This is a living document - if you see something out of date or missing, speak up!
|
||||||
|
#
|
||||||
|
# It is structured to be consumable by both humans and programs.
|
||||||
|
# To extract its contents programmatically, use any TOML-compliant parser.
|
||||||
|
#
|
||||||
|
# This file is compiled into the MAINTAINERS file in docker/opensource.
|
||||||
|
#
|
||||||
|
[Org]
|
||||||
|
[Org."Core maintainers"]
|
||||||
|
people = [
|
||||||
|
"aaronlehmann",
|
||||||
|
"calavera",
|
||||||
|
"coolljt0725",
|
||||||
|
"cpuguy83",
|
||||||
|
"crosbymichael",
|
||||||
|
"dnephin",
|
||||||
|
"dongluochen",
|
||||||
|
"duglin",
|
||||||
|
"estesp",
|
||||||
|
"icecrime",
|
||||||
|
"jhowardmsft",
|
||||||
|
"lk4d4",
|
||||||
|
"mavenugo",
|
||||||
|
"mhbauer",
|
||||||
|
"runcom",
|
||||||
|
"stevvooe",
|
||||||
|
"thajeztah",
|
||||||
|
"tianon",
|
||||||
|
"tibor",
|
||||||
|
"tonistiigi",
|
||||||
|
"unclejack",
|
||||||
|
"vdemeester",
|
||||||
|
"vieux"
|
||||||
|
]
|
||||||
|
|
||||||
|
[people]
|
||||||
|
|
||||||
|
# A reference list of all people associated with the project.
|
||||||
|
# All other sections should refer to people by their canonical key
|
||||||
|
# in the people section.
|
||||||
|
|
||||||
|
# ADD YOURSELF HERE IN ALPHABETICAL ORDER
|
||||||
|
|
||||||
|
[people.aaronlehmann]
|
||||||
|
Name = "Aaron Lehmann"
|
||||||
|
Email = "aaron.lehmann@docker.com"
|
||||||
|
GitHub = "aaronlehmann"
|
||||||
|
|
||||||
|
[people.calavera]
|
||||||
|
Name = "David Calavera"
|
||||||
|
Email = "david.calavera@gmail.com"
|
||||||
|
GitHub = "calavera"
|
||||||
|
|
||||||
|
[people.coolljt0725]
|
||||||
|
Name = "Lei Jitang"
|
||||||
|
Email = "leijitang@huawei.com"
|
||||||
|
GitHub = "coolljt0725"
|
||||||
|
|
||||||
|
[people.cpuguy83]
|
||||||
|
Name = "Brian Goff"
|
||||||
|
Email = "cpuguy83@gmail.com"
|
||||||
|
Github = "cpuguy83"
|
||||||
|
|
||||||
|
[people.crosbymichael]
|
||||||
|
Name = "Michael Crosby"
|
||||||
|
Email = "crosbymichael@gmail.com"
|
||||||
|
GitHub = "crosbymichael"
|
||||||
|
|
||||||
|
[people.dnephin]
|
||||||
|
Name = "Daniel Nephin"
|
||||||
|
Email = "dnephin@gmail.com"
|
||||||
|
GitHub = "dnephin"
|
||||||
|
|
||||||
|
[people.dongluochen]
|
||||||
|
Name = "Dongluo Chen"
|
||||||
|
Email = "dongluo.chen@docker.com"
|
||||||
|
GitHub = "dongluochen"
|
||||||
|
|
||||||
|
[people.duglin]
|
||||||
|
Name = "Doug Davis"
|
||||||
|
Email = "dug@us.ibm.com"
|
||||||
|
GitHub = "duglin"
|
||||||
|
|
||||||
|
[people.estesp]
|
||||||
|
Name = "Phil Estes"
|
||||||
|
Email = "estesp@linux.vnet.ibm.com"
|
||||||
|
GitHub = "estesp"
|
||||||
|
|
||||||
|
[people.icecrime]
|
||||||
|
Name = "Arnaud Porterie"
|
||||||
|
Email = "arnaud@docker.com"
|
||||||
|
GitHub = "icecrime"
|
||||||
|
|
||||||
|
[people.jhowardmsft]
|
||||||
|
Name = "John Howard"
|
||||||
|
Email = "jhoward@microsoft.com"
|
||||||
|
GitHub = "jhowardmsft"
|
||||||
|
|
||||||
|
[people.lk4d4]
|
||||||
|
Name = "Alexander Morozov"
|
||||||
|
Email = "lk4d4@docker.com"
|
||||||
|
GitHub = "lk4d4"
|
||||||
|
|
||||||
|
[people.mavenugo]
|
||||||
|
Name = "Madhu Venugopal"
|
||||||
|
Email = "madhu@docker.com"
|
||||||
|
GitHub = "mavenugo"
|
||||||
|
|
||||||
|
[people.mhbauer]
|
||||||
|
Name = "Morgan Bauer"
|
||||||
|
Email = "mbauer@us.ibm.com"
|
||||||
|
GitHub = "mhbauer"
|
||||||
|
|
||||||
|
[people.runcom]
|
||||||
|
Name = "Antonio Murdaca"
|
||||||
|
Email = "runcom@redhat.com"
|
||||||
|
GitHub = "runcom"
|
||||||
|
|
||||||
|
[people.stevvooe]
|
||||||
|
Name = "Stephen Day"
|
||||||
|
Email = "stephen.day@docker.com"
|
||||||
|
GitHub = "stevvooe"
|
||||||
|
|
||||||
|
[people.thajeztah]
|
||||||
|
Name = "Sebastiaan van Stijn"
|
||||||
|
Email = "github@gone.nl"
|
||||||
|
GitHub = "thaJeztah"
|
||||||
|
|
||||||
|
[people.tianon]
|
||||||
|
Name = "Tianon Gravi"
|
||||||
|
Email = "admwiggin@gmail.com"
|
||||||
|
GitHub = "tianon"
|
||||||
|
|
||||||
|
[people.tibor]
|
||||||
|
Name = "Tibor Vass"
|
||||||
|
Email = "tibor@docker.com"
|
||||||
|
GitHub = "tiborvass"
|
||||||
|
|
||||||
|
[people.tonistiigi]
|
||||||
|
Name = "Tõnis Tiigi"
|
||||||
|
Email = "tonis@docker.com"
|
||||||
|
GitHub = "tonistiigi"
|
||||||
|
|
||||||
|
[people.unclejack]
|
||||||
|
Name = "Cristian Staretu"
|
||||||
|
Email = "cristian.staretu@gmail.com"
|
||||||
|
GitHub = "unclejack"
|
||||||
|
|
||||||
|
[people.vdemeester]
|
||||||
|
Name = "Vincent Demeester"
|
||||||
|
Email = "vincent@sbr.pm"
|
||||||
|
GitHub = "vdemeester"
|
||||||
|
|
||||||
|
[people.vieux]
|
||||||
|
Name = "Victor Vieux"
|
||||||
|
Email = "vieux@docker.com"
|
||||||
|
GitHub = "vieux"
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
.PHONY: all deps osxkeychain test validate wincred
|
.PHONY: all deps osxkeychain secretservice test validate wincred
|
||||||
|
|
||||||
|
TRAVIS_OS_NAME ?= linux
|
||||||
|
|
||||||
all: test
|
all: test
|
||||||
|
|
||||||
@@ -7,17 +9,36 @@ deps:
|
|||||||
|
|
||||||
osxkeychain:
|
osxkeychain:
|
||||||
mkdir -p bin
|
mkdir -p bin
|
||||||
go build -o bin/docker-credential-osxkeychain osxkeychain/cmd/main_darwin.go
|
go build -ldflags -s -o bin/docker-credential-osxkeychain osxkeychain/cmd/main_darwin.go
|
||||||
|
|
||||||
|
codesign: osxkeychain
|
||||||
|
$(eval SIGNINGHASH = $(shell security find-identity -v -p codesigning | grep "Developer ID Application: Docker Inc" | cut -d ' ' -f 4))
|
||||||
|
xcrun -log codesign -s $(SIGNINGHASH) --force --verbose bin/docker-credential-osxkeychain
|
||||||
|
xcrun codesign --verify --deep --strict --verbose=2 --display bin/docker-credential-osxkeychain
|
||||||
|
|
||||||
|
secretservice:
|
||||||
|
mkdir -p bin
|
||||||
|
go build -o bin/docker-credential-secretservice secretservice/cmd/main_linux.go
|
||||||
|
|
||||||
|
wincred:
|
||||||
|
mkdir -p bin
|
||||||
|
go build -o bin/docker-credential-wincred.exe wincred/cmd/main_windows.go
|
||||||
|
|
||||||
test:
|
test:
|
||||||
# tests all packages except vendor
|
# tests all packages except vendor
|
||||||
go test -v `go list ./... | grep -v /vendor/`
|
go test -v `go list ./... | grep -v /vendor/`
|
||||||
|
|
||||||
validate:
|
vet: vet_$(TRAVIS_OS_NAME)
|
||||||
go vet ./credentials ./osxkeychain
|
go vet ./credentials
|
||||||
golint `go list ./... | grep -v /vendor/`
|
|
||||||
gofmt -s -l `ls **/*.go | grep -v vendor`
|
|
||||||
|
|
||||||
wincred:
|
vet_osx:
|
||||||
mkdir -p bin
|
go vet ./osxkeychain
|
||||||
go build -o bin/docker-credential-wincred wincred/cmd/main_windows.go
|
|
||||||
|
vet_linux:
|
||||||
|
go vet ./secretservice
|
||||||
|
|
||||||
|
validate: vet
|
||||||
|
for p in `go list ./... | grep -v /vendor/`; do \
|
||||||
|
golint $$p ; \
|
||||||
|
done
|
||||||
|
gofmt -s -l `ls **/*.go | grep -v vendor`
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ docker-credential-helpers is a suite of programs to use native stores to keep Do
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Go to the [Releases](https://github.com/calavera/docker-credential-helpers/releases) page and download the binary that works better for you. Put that binary in your `$PATH`, so Docker can find it.
|
Go to the [Releases](https://github.com/docker/docker-credential-helpers/releases) page and download the binary that works better for you. Put that binary in your `$PATH`, so Docker can find it.
|
||||||
|
|
||||||
### Building from scratch
|
### Building from scratch
|
||||||
|
|
||||||
@@ -13,13 +13,13 @@ The programs in this repository are written with the Go programming language. Th
|
|||||||
1 - Download the source and put it in your `$GOPATH` with `go get`.
|
1 - Download the source and put it in your `$GOPATH` with `go get`.
|
||||||
|
|
||||||
```
|
```
|
||||||
$ go get github.com/calavera/docker-credential-helpers
|
$ go get github.com/docker/docker-credential-helpers
|
||||||
```
|
```
|
||||||
|
|
||||||
2 - Use `make` to build the program you want. That will leave any executable in the `bin` directory inside the repository.
|
2 - Use `make` to build the program you want. That will leave any executable in the `bin` directory inside the repository.
|
||||||
|
|
||||||
```
|
```
|
||||||
$ cd $GOPATH/calavera/docker-credentials-helpers
|
$ cd $GOPATH/docker/docker-credentials-helpers
|
||||||
$ make osxkeychain
|
$ make osxkeychain
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -27,6 +27,8 @@ $ make osxkeychain
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
### With the Docker Engine
|
||||||
|
|
||||||
Set the `credsStore` option in your `.docker/config.json` file with the suffix of the program you want to use. For instance, set it to `osxkeychain` if you want to use `docker-credential-osxkeychain`.
|
Set the `credsStore` option in your `.docker/config.json` file with the suffix of the program you want to use. For instance, set it to `osxkeychain` if you want to use `docker-credential-osxkeychain`.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -35,18 +37,33 @@ Set the `credsStore` option in your `.docker/config.json` file with the suffix o
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### With other command line applications
|
||||||
|
|
||||||
|
The sub-package [client](https://godoc.org/github.com/docker/docker-credential-helpers/client) includes
|
||||||
|
functions to call external programs from your own command line applications.
|
||||||
|
|
||||||
|
There are three things you need to know if you need to interact with a helper:
|
||||||
|
|
||||||
|
1. The name of the program to execute, for instance `docker-credential-osxkeychain`.
|
||||||
|
2. The server address to identify the credentials, for instance `https://example.com`.
|
||||||
|
3. The username and secret to store, when you want to store credentials.
|
||||||
|
|
||||||
|
You can see examples of each function in the [client](https://godoc.org/github.com/docker/docker-credential-helpers/client) documentation.
|
||||||
|
|
||||||
### Available programs
|
### Available programs
|
||||||
|
|
||||||
1. osxkeychain: Provides a helper to use the OS X keychain as credentials store.
|
1. osxkeychain: Provides a helper to use the OS X keychain as credentials store.
|
||||||
2. wincred: Provides a helper to use Windows credentials manager as store.
|
2. secretservice: Provides a helper to use the D-Bus secret service as credentials store.
|
||||||
|
3. wincred: Provides a helper to use Windows credentials manager as store.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
A credential helper can be any program that can read values from the standard input. We use the first argument in the command line to differentiate the kind of command to execute. There are three valid values:
|
A credential helper can be any program that can read values from the standard input. We use the first argument in the command line to differentiate the kind of command to execute. There are four valid values:
|
||||||
|
|
||||||
- `store`: Adds credentials to the keychain. The payload in the standard input is a JSON document with `ServerURL`, `Username` and `Password`.
|
- `store`: Adds credentials to the keychain. The payload in the standard input is a JSON document with `ServerURL`, `Username` and `Secret`.
|
||||||
- `get`: Retrieves credentials from the keychain. The payload in the standard input is the raw value for the `ServerURL`.
|
- `get`: Retrieves credentials from the keychain. The payload in the standard input is the raw value for the `ServerURL`.
|
||||||
- `erase`: Removes credentials from the keychain. The payload in the standard input is the raw value for the `ServerURL`.
|
- `erase`: Removes credentials from the keychain. The payload in the standard input is the raw value for the `ServerURL`.
|
||||||
|
- `list`: Lists stored credentials. There is no standard input payload.
|
||||||
|
|
||||||
This repository also includes libraries to implement new credentials programs in Go. Adding a new helper program is pretty easy. You can see how the OS X keychain helper works in the [osxkeychain](osxkeychain) directory.
|
This repository also includes libraries to implement new credentials programs in Go. Adding a new helper program is pretty easy. You can see how the OS X keychain helper works in the [osxkeychain](osxkeychain) directory.
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ artifacts:
|
|||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
# All the zipped artifacts will be deployed
|
# All the zipped artifacts will be deployed
|
||||||
|
description: "Visit the [Changelog](https://github.com/docker/docker-credential-helpers/blob/master/CHANGELOG.md) for a detailed description of what's new in this release."
|
||||||
artifact: /.*\.zip/
|
artifact: /.*\.zip/
|
||||||
auth_token:
|
auth_token:
|
||||||
secure: ixWmTXZs8aV5+9s6vPXziIcdMMLd+lBVINJ0K/Sy++2wllpRxUec4/TPVKUGLqvL
|
secure: ixWmTXZs8aV5+9s6vPXziIcdMMLd+lBVINJ0K/Sy++2wllpRxUec4/TPVKUGLqvL
|
||||||
|
|||||||
+12
-3
@@ -1,6 +1,15 @@
|
|||||||
set -ex
|
set -ex
|
||||||
|
|
||||||
mkdir bin
|
mkdir bin
|
||||||
go build -o bin/docker-credential-osxkeychain osxkeychain/cmd/main_darwin.go
|
case "$TRAVIS_OS_NAME" in
|
||||||
cd bin
|
"osx")
|
||||||
tar czf ../docker-credential-osxkeychain-${TRAVIS_TAG}-amd64.tar.gz docker-credential-osxkeychain
|
go build -o bin/docker-credential-osxkeychain osxkeychain/cmd/main_darwin.go
|
||||||
|
cd bin
|
||||||
|
tar czf ../docker-credential-osxkeychain-${TRAVIS_TAG}-amd64.tar.gz docker-credential-osxkeychain
|
||||||
|
;;
|
||||||
|
"linux")
|
||||||
|
go build -o bin/docker-credential-secretservice secretservice/cmd/main_linux.go
|
||||||
|
cd bin
|
||||||
|
tar czf ../docker-credential-secretservice-${TRAVIS_TAG}-amd64.tar.gz docker-credential-secretservice
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
set -ex
|
||||||
|
|
||||||
|
sh -e /etc/init.d/xvfb start
|
||||||
|
sleep 3 # give xvfb some time to start
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/docker/docker-credential-helpers/credentials"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Store uses an external program to save credentials.
|
||||||
|
func Store(program ProgramFunc, credentials *credentials.Credentials) error {
|
||||||
|
cmd := program("store")
|
||||||
|
|
||||||
|
buffer := new(bytes.Buffer)
|
||||||
|
if err := json.NewEncoder(buffer).Encode(credentials); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cmd.Input(buffer)
|
||||||
|
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
t := strings.TrimSpace(string(out))
|
||||||
|
return fmt.Errorf("error storing credentials - err: %v, out: `%s`", err, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get executes an external program to get the credentials from a native store.
|
||||||
|
func Get(program ProgramFunc, serverURL string) (*credentials.Credentials, error) {
|
||||||
|
cmd := program("get")
|
||||||
|
cmd.Input(strings.NewReader(serverURL))
|
||||||
|
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
t := strings.TrimSpace(string(out))
|
||||||
|
|
||||||
|
if credentials.IsErrCredentialsNotFoundMessage(t) {
|
||||||
|
return nil, credentials.NewErrCredentialsNotFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("error getting credentials - err: %v, out: `%s`", err, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := &credentials.Credentials{
|
||||||
|
ServerURL: serverURL,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewDecoder(bytes.NewReader(out)).Decode(resp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Erase executes a program to remove the server credentials from the native store.
|
||||||
|
func Erase(program ProgramFunc, serverURL string) error {
|
||||||
|
cmd := program("erase")
|
||||||
|
cmd.Input(strings.NewReader(serverURL))
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
t := strings.TrimSpace(string(out))
|
||||||
|
return fmt.Errorf("error erasing credentials - err: %v, out: `%s`", err, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// List executes a program to list server credentials in the native store.
|
||||||
|
func List(program ProgramFunc) (map[string]string, error) {
|
||||||
|
cmd := program("list")
|
||||||
|
cmd.Input(strings.NewReader("unused"))
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
t := strings.TrimSpace(string(out))
|
||||||
|
return nil, fmt.Errorf("error listing credentials - err: %v, out: `%s`", err, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp map[string]string
|
||||||
|
if err = json.NewDecoder(bytes.NewReader(out)).Decode(&resp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/docker-credential-helpers/credentials"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
validServerAddress = "https://index.docker.io/v1"
|
||||||
|
validUsername = "linus"
|
||||||
|
validServerAddress2 = "https://example.com:5002"
|
||||||
|
invalidServerAddress = "https://foobar.example.com"
|
||||||
|
missingCredsAddress = "https://missing.docker.io/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
var errProgramExited = fmt.Errorf("exited 1")
|
||||||
|
|
||||||
|
// mockProgram simulates interactions between the docker client and a remote
|
||||||
|
// credentials helper.
|
||||||
|
// Unit tests inject this mocked command into the remote to control execution.
|
||||||
|
type mockProgram struct {
|
||||||
|
arg string
|
||||||
|
input io.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output returns responses from the remote credentials helper.
|
||||||
|
// It mocks those responses based in the input in the mock.
|
||||||
|
func (m *mockProgram) Output() ([]byte, error) {
|
||||||
|
in, err := ioutil.ReadAll(m.input)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
inS := string(in)
|
||||||
|
|
||||||
|
switch m.arg {
|
||||||
|
case "erase":
|
||||||
|
switch inS {
|
||||||
|
case validServerAddress:
|
||||||
|
return nil, nil
|
||||||
|
default:
|
||||||
|
return []byte("program failed"), errProgramExited
|
||||||
|
}
|
||||||
|
case "get":
|
||||||
|
switch inS {
|
||||||
|
case validServerAddress:
|
||||||
|
return []byte(`{"Username": "foo", "Secret": "bar"}`), nil
|
||||||
|
case validServerAddress2:
|
||||||
|
return []byte(`{"Username": "<token>", "Secret": "abcd1234"}`), nil
|
||||||
|
case missingCredsAddress:
|
||||||
|
return []byte(credentials.NewErrCredentialsNotFound().Error()), errProgramExited
|
||||||
|
case invalidServerAddress:
|
||||||
|
return []byte("program failed"), errProgramExited
|
||||||
|
}
|
||||||
|
case "store":
|
||||||
|
var c credentials.Credentials
|
||||||
|
err := json.NewDecoder(strings.NewReader(inS)).Decode(&c)
|
||||||
|
if err != nil {
|
||||||
|
return []byte("error storing credentials"), errProgramExited
|
||||||
|
}
|
||||||
|
switch c.ServerURL {
|
||||||
|
case validServerAddress:
|
||||||
|
return nil, nil
|
||||||
|
case validServerAddress2:
|
||||||
|
return nil, nil
|
||||||
|
default:
|
||||||
|
return []byte("error storing credentials"), errProgramExited
|
||||||
|
}
|
||||||
|
case "list":
|
||||||
|
return []byte(fmt.Sprintf(`{"%s": "%s"}`, validServerAddress, validUsername)), nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return []byte(fmt.Sprintf("unknown argument %q with %q", m.arg, inS)), errProgramExited
|
||||||
|
}
|
||||||
|
|
||||||
|
// Input sets the input to send to a remote credentials helper.
|
||||||
|
func (m *mockProgram) Input(in io.Reader) {
|
||||||
|
m.input = in
|
||||||
|
}
|
||||||
|
|
||||||
|
func mockProgramFn(args ...string) Program {
|
||||||
|
return &mockProgram{
|
||||||
|
arg: args[0],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleStore() {
|
||||||
|
p := NewShellProgramFunc("docker-credential-secretservice")
|
||||||
|
|
||||||
|
c := &credentials.Credentials{
|
||||||
|
ServerURL: "https://example.com",
|
||||||
|
Username: "calavera",
|
||||||
|
Secret: "my super secret token",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := Store(p, c); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStore(t *testing.T) {
|
||||||
|
valid := []credentials.Credentials{
|
||||||
|
{validServerAddress, "foo", "bar"},
|
||||||
|
{validServerAddress2, "<token>", "abcd1234"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range valid {
|
||||||
|
if err := Store(mockProgramFn, &v); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
invalid := []credentials.Credentials{
|
||||||
|
{invalidServerAddress, "foo", "bar"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range invalid {
|
||||||
|
if err := Store(mockProgramFn, &v); err == nil {
|
||||||
|
t.Fatalf("Expected error for server %s, got nil", v.ServerURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleGet() {
|
||||||
|
p := NewShellProgramFunc("docker-credential-secretservice")
|
||||||
|
|
||||||
|
creds, err := Get(p, "https://example.com")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Got credentials for user `%s` in `%s`\n", creds.Username, creds.ServerURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGet(t *testing.T) {
|
||||||
|
valid := []credentials.Credentials{
|
||||||
|
{validServerAddress, "foo", "bar"},
|
||||||
|
{validServerAddress2, "<token>", "abcd1234"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range valid {
|
||||||
|
c, err := Get(mockProgramFn, v.ServerURL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Username != v.Username {
|
||||||
|
t.Fatalf("expected username `%s`, got %s", v.Username, c.Username)
|
||||||
|
}
|
||||||
|
if c.Secret != v.Secret {
|
||||||
|
t.Fatalf("expected secret `%s`, got %s", v.Secret, c.Secret)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
invalid := []struct {
|
||||||
|
serverURL string
|
||||||
|
err string
|
||||||
|
}{
|
||||||
|
{missingCredsAddress, credentials.NewErrCredentialsNotFound().Error()},
|
||||||
|
{invalidServerAddress, "error getting credentials - err: exited 1, out: `program failed`"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range invalid {
|
||||||
|
_, err := Get(mockProgramFn, v.serverURL)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected error for server %s, got nil", v.serverURL)
|
||||||
|
}
|
||||||
|
if err.Error() != v.err {
|
||||||
|
t.Fatalf("Expected error `%s`, got `%v`", v.err, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleErase() {
|
||||||
|
p := NewShellProgramFunc("docker-credential-secretservice")
|
||||||
|
|
||||||
|
if err := Erase(p, "https://example.com"); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestErase(t *testing.T) {
|
||||||
|
if err := Erase(mockProgramFn, validServerAddress); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := Erase(mockProgramFn, invalidServerAddress); err == nil {
|
||||||
|
t.Fatalf("Expected error for server %s, got nil", invalidServerAddress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestList(t *testing.T) {
|
||||||
|
auths, err := List(mockProgramFn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if username, exists := auths[validServerAddress]; !exists || username != validUsername {
|
||||||
|
t.Fatalf("auths[%s] returned %s, %t; expected %s, %t", validServerAddress, username, exists, validUsername, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Program is an interface to execute external programs.
|
||||||
|
type Program interface {
|
||||||
|
Output() ([]byte, error)
|
||||||
|
Input(in io.Reader)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProgramFunc is a type of function that initializes programs based on arguments.
|
||||||
|
type ProgramFunc func(args ...string) Program
|
||||||
|
|
||||||
|
// NewShellProgramFunc creates programs that are executed in a Shell.
|
||||||
|
func NewShellProgramFunc(name string) ProgramFunc {
|
||||||
|
return func(args ...string) Program {
|
||||||
|
return &Shell{cmd: exec.Command(name, args...)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shell invokes shell commands to talk with a remote credentials helper.
|
||||||
|
type Shell struct {
|
||||||
|
cmd *exec.Cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output returns responses from the remote credentials helper.
|
||||||
|
func (s *Shell) Output() ([]byte, error) {
|
||||||
|
return s.cmd.Output()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Input sets the input to send to a remote credentials helper.
|
||||||
|
func (s *Shell) Input(in io.Reader) {
|
||||||
|
s.cmd.Stdin = in
|
||||||
|
}
|
||||||
+50
-19
@@ -10,36 +10,52 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type credentialsGetResponse struct {
|
// Credentials holds the information shared between docker and the credentials store.
|
||||||
|
type Credentials struct {
|
||||||
|
ServerURL string
|
||||||
Username string
|
Username string
|
||||||
Password string
|
Secret string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serve initializes the credentials helper and parses the action argument.
|
// Serve initializes the credentials helper and parses the action argument.
|
||||||
|
// This function is designed to be called from a command line interface.
|
||||||
|
// It uses os.Args[1] as the key for the action.
|
||||||
|
// It uses os.Stdin as input and os.Stdout as output.
|
||||||
|
// This function terminates the program with os.Exit(1) if there is an error.
|
||||||
func Serve(helper Helper) {
|
func Serve(helper Helper) {
|
||||||
if err := handleCommand(helper); err != nil {
|
var err error
|
||||||
|
if len(os.Args) != 2 {
|
||||||
|
err = fmt.Errorf("Usage: %s <store|get|erase|list>", os.Args[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
err = HandleCommand(helper, os.Args[1], os.Stdin, os.Stdout)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stdout, "%v\n", err)
|
fmt.Fprintf(os.Stdout, "%v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleCommand(helper Helper) error {
|
// HandleCommand uses a helper and a key to run a credential action.
|
||||||
if len(os.Args) != 2 {
|
func HandleCommand(helper Helper, key string, in io.Reader, out io.Writer) error {
|
||||||
return fmt.Errorf("Usage: %s <store|get|erase>", os.Args[0])
|
switch key {
|
||||||
}
|
|
||||||
|
|
||||||
switch os.Args[1] {
|
|
||||||
case "store":
|
case "store":
|
||||||
return store(helper, os.Stdin)
|
return Store(helper, in)
|
||||||
case "get":
|
case "get":
|
||||||
return get(helper, os.Stdin, os.Stdout)
|
return Get(helper, in, out)
|
||||||
case "erase":
|
case "erase":
|
||||||
return erase(helper, os.Stdin)
|
return Erase(helper, in)
|
||||||
|
case "list":
|
||||||
|
return List(helper, out)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("Usage: %s <store|get|erase>", os.Args[0])
|
return fmt.Errorf("Unknown credential action `%s`", key)
|
||||||
}
|
}
|
||||||
|
|
||||||
func store(helper Helper, reader io.Reader) error {
|
// Store uses a helper and an input reader to save credentials.
|
||||||
|
// The reader must contain the JSON serialization of a Credentials struct.
|
||||||
|
func Store(helper Helper, reader io.Reader) error {
|
||||||
scanner := bufio.NewScanner(reader)
|
scanner := bufio.NewScanner(reader)
|
||||||
|
|
||||||
buffer := new(bytes.Buffer)
|
buffer := new(bytes.Buffer)
|
||||||
@@ -59,7 +75,10 @@ func store(helper Helper, reader io.Reader) error {
|
|||||||
return helper.Add(&creds)
|
return helper.Add(&creds)
|
||||||
}
|
}
|
||||||
|
|
||||||
func get(helper Helper, reader io.Reader, writer io.Writer) error {
|
// Get retrieves the credentials for a given server url.
|
||||||
|
// The reader must contain the server URL to search.
|
||||||
|
// The writer is used to write the JSON serialization of the credentials.
|
||||||
|
func Get(helper Helper, reader io.Reader, writer io.Writer) error {
|
||||||
scanner := bufio.NewScanner(reader)
|
scanner := bufio.NewScanner(reader)
|
||||||
|
|
||||||
buffer := new(bytes.Buffer)
|
buffer := new(bytes.Buffer)
|
||||||
@@ -73,14 +92,14 @@ func get(helper Helper, reader io.Reader, writer io.Writer) error {
|
|||||||
|
|
||||||
serverURL := strings.TrimSpace(buffer.String())
|
serverURL := strings.TrimSpace(buffer.String())
|
||||||
|
|
||||||
username, password, err := helper.Get(serverURL)
|
username, secret, err := helper.Get(serverURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := credentialsGetResponse{
|
resp := Credentials{
|
||||||
Username: username,
|
Username: username,
|
||||||
Password: password,
|
Secret: secret,
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer.Reset()
|
buffer.Reset()
|
||||||
@@ -92,7 +111,9 @@ func get(helper Helper, reader io.Reader, writer io.Writer) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func erase(helper Helper, reader io.Reader) error {
|
// Erase removes credentials from the store.
|
||||||
|
// The reader must contain the server URL to remove.
|
||||||
|
func Erase(helper Helper, reader io.Reader) error {
|
||||||
scanner := bufio.NewScanner(reader)
|
scanner := bufio.NewScanner(reader)
|
||||||
|
|
||||||
buffer := new(bytes.Buffer)
|
buffer := new(bytes.Buffer)
|
||||||
@@ -108,3 +129,13 @@ func erase(helper Helper, reader io.Reader) error {
|
|||||||
|
|
||||||
return helper.Delete(serverURL)
|
return helper.Delete(serverURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//List returns all the serverURLs of keys in
|
||||||
|
//the OS store as a list of strings
|
||||||
|
func List(helper Helper, writer io.Writer) error {
|
||||||
|
accts, err := helper.List()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return json.NewEncoder(writer).Encode(accts)
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,7 +33,12 @@ func (m *memoryStore) Get(serverURL string) (string, string, error) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return "", "", fmt.Errorf("creds not found for %s", serverURL)
|
return "", "", fmt.Errorf("creds not found for %s", serverURL)
|
||||||
}
|
}
|
||||||
return c.Username, c.Password, nil
|
return c.Username, c.Secret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memoryStore) List() (map[string]string, error) {
|
||||||
|
//Simply a placeholder to let memoryStore be a valid implementation of Helper interface
|
||||||
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStore(t *testing.T) {
|
func TestStore(t *testing.T) {
|
||||||
@@ -41,7 +46,7 @@ func TestStore(t *testing.T) {
|
|||||||
creds := &Credentials{
|
creds := &Credentials{
|
||||||
ServerURL: serverURL,
|
ServerURL: serverURL,
|
||||||
Username: "foo",
|
Username: "foo",
|
||||||
Password: "bar",
|
Secret: "bar",
|
||||||
}
|
}
|
||||||
b, err := json.Marshal(creds)
|
b, err := json.Marshal(creds)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -50,7 +55,7 @@ func TestStore(t *testing.T) {
|
|||||||
in := bytes.NewReader(b)
|
in := bytes.NewReader(b)
|
||||||
|
|
||||||
h := newMemoryStore()
|
h := newMemoryStore()
|
||||||
if err := store(h, in); err != nil {
|
if err := Store(h, in); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,8 +68,8 @@ func TestStore(t *testing.T) {
|
|||||||
t.Fatalf("expected username foo, got %s\n", c.Username)
|
t.Fatalf("expected username foo, got %s\n", c.Username)
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Password != "bar" {
|
if c.Secret != "bar" {
|
||||||
t.Fatalf("expected username bar, got %s\n", c.Password)
|
t.Fatalf("expected username bar, got %s\n", c.Secret)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,7 +78,7 @@ func TestGet(t *testing.T) {
|
|||||||
creds := &Credentials{
|
creds := &Credentials{
|
||||||
ServerURL: serverURL,
|
ServerURL: serverURL,
|
||||||
Username: "foo",
|
Username: "foo",
|
||||||
Password: "bar",
|
Secret: "bar",
|
||||||
}
|
}
|
||||||
b, err := json.Marshal(creds)
|
b, err := json.Marshal(creds)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -82,13 +87,13 @@ func TestGet(t *testing.T) {
|
|||||||
in := bytes.NewReader(b)
|
in := bytes.NewReader(b)
|
||||||
|
|
||||||
h := newMemoryStore()
|
h := newMemoryStore()
|
||||||
if err := store(h, in); err != nil {
|
if err := Store(h, in); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
buf := strings.NewReader(serverURL)
|
buf := strings.NewReader(serverURL)
|
||||||
w := new(bytes.Buffer)
|
w := new(bytes.Buffer)
|
||||||
if err := get(h, buf, w); err != nil {
|
if err := Get(h, buf, w); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +101,7 @@ func TestGet(t *testing.T) {
|
|||||||
t.Fatalf("expected output in the writer, got %d", w.Len())
|
t.Fatalf("expected output in the writer, got %d", w.Len())
|
||||||
}
|
}
|
||||||
|
|
||||||
var c credentialsGetResponse
|
var c Credentials
|
||||||
if err := json.NewDecoder(w).Decode(&c); err != nil {
|
if err := json.NewDecoder(w).Decode(&c); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -105,8 +110,8 @@ func TestGet(t *testing.T) {
|
|||||||
t.Fatalf("expected username foo, got %s\n", c.Username)
|
t.Fatalf("expected username foo, got %s\n", c.Username)
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Password != "bar" {
|
if c.Secret != "bar" {
|
||||||
t.Fatalf("expected username bar, got %s\n", c.Password)
|
t.Fatalf("expected username bar, got %s\n", c.Secret)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,7 +120,7 @@ func TestErase(t *testing.T) {
|
|||||||
creds := &Credentials{
|
creds := &Credentials{
|
||||||
ServerURL: serverURL,
|
ServerURL: serverURL,
|
||||||
Username: "foo",
|
Username: "foo",
|
||||||
Password: "bar",
|
Secret: "bar",
|
||||||
}
|
}
|
||||||
b, err := json.Marshal(creds)
|
b, err := json.Marshal(creds)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -124,17 +129,31 @@ func TestErase(t *testing.T) {
|
|||||||
in := bytes.NewReader(b)
|
in := bytes.NewReader(b)
|
||||||
|
|
||||||
h := newMemoryStore()
|
h := newMemoryStore()
|
||||||
if err := store(h, in); err != nil {
|
if err := Store(h, in); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
buf := strings.NewReader(serverURL)
|
buf := strings.NewReader(serverURL)
|
||||||
if err := erase(h, buf); err != nil {
|
if err := Erase(h, buf); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
w := new(bytes.Buffer)
|
w := new(bytes.Buffer)
|
||||||
if err := get(h, buf, w); err == nil {
|
if err := Get(h, buf, w); err == nil {
|
||||||
t.Fatal("expected error getting missing creds, got empty")
|
t.Fatal("expected error getting missing creds, got empty")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestList(t *testing.T) {
|
||||||
|
//This tests that there is proper input an output into the byte stream
|
||||||
|
//Individual stores are very OS specific and have been tested in osxkeychain and secretservice respectively
|
||||||
|
out := new(bytes.Buffer)
|
||||||
|
h := newMemoryStore()
|
||||||
|
if err := List(h, out); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
//testing that there is an output
|
||||||
|
if out.Len() == 0 {
|
||||||
|
t.Fatalf("expected output in the writer, got %d", 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package credentials
|
||||||
|
|
||||||
|
// ErrCredentialsNotFound standarizes the not found error, so every helper returns
|
||||||
|
// the same message and docker can handle it properly.
|
||||||
|
const errCredentialsNotFoundMessage = "credentials not found in native keychain"
|
||||||
|
|
||||||
|
// errCredentialsNotFound represents an error
|
||||||
|
// raised when credentials are not in the store.
|
||||||
|
type errCredentialsNotFound struct{}
|
||||||
|
|
||||||
|
// Error returns the standard error message
|
||||||
|
// for when the credentials are not in the store.
|
||||||
|
func (errCredentialsNotFound) Error() string {
|
||||||
|
return errCredentialsNotFoundMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewErrCredentialsNotFound creates a new error
|
||||||
|
// for when the credentials are not in the store.
|
||||||
|
func NewErrCredentialsNotFound() error {
|
||||||
|
return errCredentialsNotFound{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsErrCredentialsNotFound returns true if the error
|
||||||
|
// was caused by not having a set of credentials in a store.
|
||||||
|
func IsErrCredentialsNotFound(err error) bool {
|
||||||
|
_, ok := err.(errCredentialsNotFound)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsErrCredentialsNotFoundMessage returns true if the error
|
||||||
|
// was caused by not having a set of credentials in a store.
|
||||||
|
//
|
||||||
|
// This function helps to check messages returned by an
|
||||||
|
// external program via its standard output.
|
||||||
|
func IsErrCredentialsNotFoundMessage(err string) bool {
|
||||||
|
return err == errCredentialsNotFoundMessage
|
||||||
|
}
|
||||||
+6
-13
@@ -1,21 +1,14 @@
|
|||||||
package credentials
|
package credentials
|
||||||
|
|
||||||
import "errors"
|
|
||||||
|
|
||||||
// Credentials holds the information shared between docker and the credentials store.
|
|
||||||
type Credentials struct {
|
|
||||||
ServerURL string
|
|
||||||
Username string
|
|
||||||
Password string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper is the interface a credentials store helper must implement.
|
// Helper is the interface a credentials store helper must implement.
|
||||||
type Helper interface {
|
type Helper interface {
|
||||||
|
// Add appends credentials to the store.
|
||||||
Add(*Credentials) error
|
Add(*Credentials) error
|
||||||
|
// Delete removes credentials from the store.
|
||||||
Delete(serverURL string) error
|
Delete(serverURL string) error
|
||||||
|
// Get retrieves credentials from the store.
|
||||||
|
// It returns username and secret as strings.
|
||||||
Get(serverURL string) (string, string, error)
|
Get(serverURL string) (string, string, error)
|
||||||
|
// List returns the stored serverURLs and their associated usernames.
|
||||||
|
List() (map[string]string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrCredentialsNotFound standarizes the not found error, so every helper returns
|
|
||||||
// the same message and docker can handle it properly.
|
|
||||||
var ErrCredentialsNotFound = errors.New("credentials not found in native keychain")
|
|
||||||
|
|||||||
@@ -6,5 +6,5 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
credentials.Serve(osxkeychain.New())
|
credentials.Serve(osxkeychain.Osxkeychain{})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
#include "osxkeychain_darwin.h"
|
#include "osxkeychain_darwin.h"
|
||||||
|
#include <CoreFoundation/CoreFoundation.h>
|
||||||
|
#include <Foundation/NSValue.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
char *get_error(OSStatus status) {
|
char *get_error(OSStatus status) {
|
||||||
char *buf = malloc(128);
|
char *buf = malloc(128);
|
||||||
@@ -10,7 +14,7 @@ char *get_error(OSStatus status) {
|
|||||||
return buf;
|
return buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
char *keychain_add(struct Server *server, char *username, char *password) {
|
char *keychain_add(struct Server *server, char *username, char *secret) {
|
||||||
OSStatus status = SecKeychainAddInternetPassword(
|
OSStatus status = SecKeychainAddInternetPassword(
|
||||||
NULL,
|
NULL,
|
||||||
strlen(server->host), server->host,
|
strlen(server->host), server->host,
|
||||||
@@ -20,7 +24,7 @@ char *keychain_add(struct Server *server, char *username, char *password) {
|
|||||||
server->port,
|
server->port,
|
||||||
server->proto,
|
server->proto,
|
||||||
kSecAuthenticationTypeDefault,
|
kSecAuthenticationTypeDefault,
|
||||||
strlen(password), password,
|
strlen(secret), secret,
|
||||||
NULL
|
NULL
|
||||||
);
|
);
|
||||||
if (status) {
|
if (status) {
|
||||||
@@ -29,7 +33,7 @@ char *keychain_add(struct Server *server, char *username, char *password) {
|
|||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
char *keychain_get(struct Server *server, unsigned int *username_l, char **username, unsigned int *password_l, char **password) {
|
char *keychain_get(struct Server *server, unsigned int *username_l, char **username, unsigned int *secret_l, char **secret) {
|
||||||
char *tmp;
|
char *tmp;
|
||||||
SecKeychainItemRef item;
|
SecKeychainItemRef item;
|
||||||
|
|
||||||
@@ -42,14 +46,14 @@ char *keychain_get(struct Server *server, unsigned int *username_l, char **usern
|
|||||||
server->port,
|
server->port,
|
||||||
server->proto,
|
server->proto,
|
||||||
kSecAuthenticationTypeDefault,
|
kSecAuthenticationTypeDefault,
|
||||||
password_l, (void **)&tmp,
|
secret_l, (void **)&tmp,
|
||||||
&item);
|
&item);
|
||||||
|
|
||||||
if (status) {
|
if (status) {
|
||||||
return get_error(status);
|
return get_error(status);
|
||||||
}
|
}
|
||||||
|
|
||||||
*password = strdup(tmp);
|
*secret = strdup(tmp);
|
||||||
SecKeychainItemFreeContent(NULL, tmp);
|
SecKeychainItemFreeContent(NULL, tmp);
|
||||||
|
|
||||||
SecKeychainAttributeList list;
|
SecKeychainAttributeList list;
|
||||||
@@ -96,3 +100,106 @@ char *keychain_delete(struct Server *server) {
|
|||||||
}
|
}
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
char * CFStringToCharArr(CFStringRef aString) {
|
||||||
|
if (aString == NULL) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
CFIndex length = CFStringGetLength(aString);
|
||||||
|
CFIndex maxSize =
|
||||||
|
CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1;
|
||||||
|
char *buffer = (char *)malloc(maxSize);
|
||||||
|
if (CFStringGetCString(aString, buffer, maxSize,
|
||||||
|
kCFStringEncodingUTF8)) {
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
char *keychain_list(char *** paths, char *** accts, unsigned int *list_l) {
|
||||||
|
CFMutableDictionaryRef query = CFDictionaryCreateMutable (NULL, 1, NULL, NULL);
|
||||||
|
CFDictionaryAddValue(query, kSecClass, kSecClassInternetPassword);
|
||||||
|
CFDictionaryAddValue(query, kSecReturnAttributes, kCFBooleanTrue);
|
||||||
|
CFDictionaryAddValue(query, kSecMatchLimit, kSecMatchLimitAll);
|
||||||
|
//Use this query dictionary
|
||||||
|
CFTypeRef result= NULL;
|
||||||
|
OSStatus status = SecItemCopyMatching(
|
||||||
|
query,
|
||||||
|
&result);
|
||||||
|
//Ran a search and store the results in result
|
||||||
|
if (status) {
|
||||||
|
return get_error(status);
|
||||||
|
}
|
||||||
|
CFIndex numKeys = CFArrayGetCount(result);
|
||||||
|
*paths = (char **) malloc((int)sizeof(char *)*numKeys);
|
||||||
|
*accts = (char **) malloc((int)sizeof(char *)*numKeys);
|
||||||
|
//result is of type CFArray
|
||||||
|
for(CFIndex i=0; i<numKeys; i++) {
|
||||||
|
CFDictionaryRef currKey = CFArrayGetValueAtIndex(result,i);
|
||||||
|
|
||||||
|
CFStringRef protocolTmp = CFDictionaryGetValue(currKey, CFSTR("ptcl"));
|
||||||
|
if (protocolTmp != NULL) {
|
||||||
|
CFStringRef protocolStr = CFStringCreateWithFormat(NULL, NULL, CFSTR("%@"), protocolTmp);
|
||||||
|
if (CFStringCompare(protocolStr, CFSTR("htps"), 0) == kCFCompareEqualTo) {
|
||||||
|
protocolTmp = CFSTR("https://");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
protocolTmp = CFSTR("http://");
|
||||||
|
}
|
||||||
|
CFRelease(protocolStr);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
char * path = "0";
|
||||||
|
char * acct = "0";
|
||||||
|
(*paths)[i] = (char *) malloc(sizeof(char)*(strlen(path)));
|
||||||
|
memcpy((*paths)[i], path, sizeof(char)*(strlen(path)));
|
||||||
|
(*accts)[i] = (char *) malloc(sizeof(char)*(strlen(acct)));
|
||||||
|
memcpy((*accts)[i], acct, sizeof(char)*(strlen(acct)));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
CFMutableStringRef str = CFStringCreateMutableCopy(NULL, 0, protocolTmp);
|
||||||
|
CFStringRef serverTmp = CFDictionaryGetValue(currKey, CFSTR("srvr"));
|
||||||
|
if (serverTmp != NULL) {
|
||||||
|
CFStringAppend(str, serverTmp);
|
||||||
|
}
|
||||||
|
|
||||||
|
CFStringRef pathTmp = CFDictionaryGetValue(currKey, CFSTR("path"));
|
||||||
|
if (pathTmp != NULL) {
|
||||||
|
CFStringAppend(str, pathTmp);
|
||||||
|
}
|
||||||
|
|
||||||
|
const NSNumber * portTmp = CFDictionaryGetValue(currKey, CFSTR("port"));
|
||||||
|
if (portTmp != NULL && portTmp.integerValue != 0) {
|
||||||
|
CFStringRef portStr = CFStringCreateWithFormat(NULL, NULL, CFSTR("%@"), portTmp);
|
||||||
|
CFStringAppend(str, CFSTR(":"));
|
||||||
|
CFStringAppend(str, portStr);
|
||||||
|
CFRelease(portStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
CFStringRef acctTmp = CFDictionaryGetValue(currKey, CFSTR("acct"));
|
||||||
|
if (acctTmp == NULL) {
|
||||||
|
acctTmp = CFSTR("account not defined");
|
||||||
|
}
|
||||||
|
|
||||||
|
char * path = CFStringToCharArr(str);
|
||||||
|
char * acct = CFStringToCharArr(acctTmp);
|
||||||
|
|
||||||
|
//We now have all we need, username and servername. Now export this to .go
|
||||||
|
(*paths)[i] = (char *) malloc(sizeof(char)*(strlen(path)+1));
|
||||||
|
memcpy((*paths)[i], path, sizeof(char)*(strlen(path)+1));
|
||||||
|
(*accts)[i] = (char *) malloc(sizeof(char)*(strlen(acct)+1));
|
||||||
|
memcpy((*accts)[i], acct, sizeof(char)*(strlen(acct)+1));
|
||||||
|
|
||||||
|
CFRelease(str);
|
||||||
|
}
|
||||||
|
*list_l = (int)numKeys;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
void freeListData(char *** data, unsigned int length) {
|
||||||
|
for(int i=0; i<length; i++) {
|
||||||
|
free((*data)[i]);
|
||||||
|
}
|
||||||
|
free(*data);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package osxkeychain
|
package osxkeychain
|
||||||
|
|
||||||
/*
|
/*
|
||||||
#cgo CFLAGS: -x objective-c
|
#cgo CFLAGS: -x objective-c -mmacosx-version-min=10.10
|
||||||
#cgo LDFLAGS: -framework Security -framework Foundation
|
#cgo LDFLAGS: -framework Security -framework Foundation -mmacosx-version-min=10.10
|
||||||
|
|
||||||
#include "osxkeychain_darwin.h"
|
#include "osxkeychain_darwin.h"
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -22,15 +22,13 @@ import (
|
|||||||
// when the credentials are not in the keychain.
|
// when the credentials are not in the keychain.
|
||||||
const errCredentialsNotFound = "The specified item could not be found in the keychain."
|
const errCredentialsNotFound = "The specified item could not be found in the keychain."
|
||||||
|
|
||||||
type osxkeychain struct{}
|
// Osxkeychain handles secrets using the OS X Keychain as store.
|
||||||
|
type Osxkeychain struct{}
|
||||||
// New creates a new osxkeychain.
|
|
||||||
func New() credentials.Helper {
|
|
||||||
return osxkeychain{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add adds new credentials to the keychain.
|
// Add adds new credentials to the keychain.
|
||||||
func (h osxkeychain) Add(creds *credentials.Credentials) error {
|
func (h Osxkeychain) Add(creds *credentials.Credentials) error {
|
||||||
|
h.Delete(creds.ServerURL)
|
||||||
|
|
||||||
s, err := splitServer(creds.ServerURL)
|
s, err := splitServer(creds.ServerURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -39,10 +37,10 @@ func (h osxkeychain) Add(creds *credentials.Credentials) error {
|
|||||||
|
|
||||||
username := C.CString(creds.Username)
|
username := C.CString(creds.Username)
|
||||||
defer C.free(unsafe.Pointer(username))
|
defer C.free(unsafe.Pointer(username))
|
||||||
password := C.CString(creds.Password)
|
secret := C.CString(creds.Secret)
|
||||||
defer C.free(unsafe.Pointer(password))
|
defer C.free(unsafe.Pointer(secret))
|
||||||
|
|
||||||
errMsg := C.keychain_add(s, username, password)
|
errMsg := C.keychain_add(s, username, secret)
|
||||||
if errMsg != nil {
|
if errMsg != nil {
|
||||||
defer C.free(unsafe.Pointer(errMsg))
|
defer C.free(unsafe.Pointer(errMsg))
|
||||||
return errors.New(C.GoString(errMsg))
|
return errors.New(C.GoString(errMsg))
|
||||||
@@ -52,7 +50,7 @@ func (h osxkeychain) Add(creds *credentials.Credentials) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Delete removes credentials from the keychain.
|
// Delete removes credentials from the keychain.
|
||||||
func (h osxkeychain) Delete(serverURL string) error {
|
func (h Osxkeychain) Delete(serverURL string) error {
|
||||||
s, err := splitServer(serverURL)
|
s, err := splitServer(serverURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -68,8 +66,8 @@ func (h osxkeychain) Delete(serverURL string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get returns the username and password to use for a given registry server URL.
|
// Get returns the username and secret to use for a given registry server URL.
|
||||||
func (h osxkeychain) Get(serverURL string) (string, string, error) {
|
func (h Osxkeychain) Get(serverURL string) (string, string, error) {
|
||||||
s, err := splitServer(serverURL)
|
s, err := splitServer(serverURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
@@ -78,28 +76,59 @@ func (h osxkeychain) Get(serverURL string) (string, string, error) {
|
|||||||
|
|
||||||
var usernameLen C.uint
|
var usernameLen C.uint
|
||||||
var username *C.char
|
var username *C.char
|
||||||
var passwordLen C.uint
|
var secretLen C.uint
|
||||||
var password *C.char
|
var secret *C.char
|
||||||
defer C.free(unsafe.Pointer(username))
|
defer C.free(unsafe.Pointer(username))
|
||||||
defer C.free(unsafe.Pointer(password))
|
defer C.free(unsafe.Pointer(secret))
|
||||||
|
|
||||||
errMsg := C.keychain_get(s, &usernameLen, &username, &passwordLen, &password)
|
errMsg := C.keychain_get(s, &usernameLen, &username, &secretLen, &secret)
|
||||||
if errMsg != nil {
|
if errMsg != nil {
|
||||||
defer C.free(unsafe.Pointer(errMsg))
|
defer C.free(unsafe.Pointer(errMsg))
|
||||||
goMsg := C.GoString(errMsg)
|
goMsg := C.GoString(errMsg)
|
||||||
|
|
||||||
if goMsg == errCredentialsNotFound {
|
if goMsg == errCredentialsNotFound {
|
||||||
return "", "", credentials.ErrCredentialsNotFound
|
return "", "", credentials.NewErrCredentialsNotFound()
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", "", errors.New(goMsg)
|
return "", "", errors.New(goMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
user := C.GoStringN(username, C.int(usernameLen))
|
user := C.GoStringN(username, C.int(usernameLen))
|
||||||
pass := C.GoStringN(password, C.int(passwordLen))
|
pass := C.GoStringN(secret, C.int(secretLen))
|
||||||
return user, pass, nil
|
return user, pass, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// List returns the stored URLs and corresponding usernames.
|
||||||
|
func (h Osxkeychain) List() (map[string]string, error) {
|
||||||
|
var pathsC **C.char
|
||||||
|
defer C.free(unsafe.Pointer(pathsC))
|
||||||
|
var acctsC **C.char
|
||||||
|
defer C.free(unsafe.Pointer(acctsC))
|
||||||
|
var listLenC C.uint
|
||||||
|
errMsg := C.keychain_list(&pathsC, &acctsC, &listLenC)
|
||||||
|
if errMsg != nil {
|
||||||
|
defer C.free(unsafe.Pointer(errMsg))
|
||||||
|
goMsg := C.GoString(errMsg)
|
||||||
|
return nil, errors.New(goMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer C.freeListData(&pathsC, listLenC)
|
||||||
|
defer C.freeListData(&acctsC, listLenC)
|
||||||
|
|
||||||
|
var listLen int
|
||||||
|
listLen = int(listLenC)
|
||||||
|
pathTmp := (*[1 << 30]*C.char)(unsafe.Pointer(pathsC))[:listLen:listLen]
|
||||||
|
acctTmp := (*[1 << 30]*C.char)(unsafe.Pointer(acctsC))[:listLen:listLen]
|
||||||
|
//taking the array of c strings into go while ignoring all the stuff irrelevant to credentials-helper
|
||||||
|
resp := make(map[string]string)
|
||||||
|
for i := 0; i < listLen; i++ {
|
||||||
|
if C.GoString(pathTmp[i]) == "0" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
resp[C.GoString(pathTmp[i])] = C.GoString(acctTmp[i])
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
func splitServer(serverURL string) (*C.struct_Server, error) {
|
func splitServer(serverURL string) (*C.struct_Server, error) {
|
||||||
u, err := url.Parse(serverURL)
|
u, err := url.Parse(serverURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ struct Server {
|
|||||||
unsigned int port;
|
unsigned int port;
|
||||||
};
|
};
|
||||||
|
|
||||||
char *keychain_add(struct Server *server, char *username, char *password);
|
char *keychain_add(struct Server *server, char *username, char *secret);
|
||||||
char *keychain_get(struct Server *server, unsigned int *username_l, char **username, unsigned int *password_l, char **password);
|
char *keychain_get(struct Server *server, unsigned int *username_l, char **username, unsigned int *secret_l, char **secret);
|
||||||
char *keychain_delete(struct Server *server);
|
char *keychain_delete(struct Server *server);
|
||||||
|
char *keychain_list(char *** data, char *** accts, unsigned int *list_l);
|
||||||
|
void freeListData(char *** data, unsigned int length);
|
||||||
@@ -1,24 +1,27 @@
|
|||||||
package osxkeychain
|
package osxkeychain
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/docker/docker-credential-helpers/credentials"
|
"github.com/docker/docker-credential-helpers/credentials"
|
||||||
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestOSXKeychainHelper(t *testing.T) {
|
func TestOSXKeychainHelper(t *testing.T) {
|
||||||
creds := &credentials.Credentials{
|
creds := &credentials.Credentials{
|
||||||
ServerURL: "https://foobar.docker.io:2376/v1",
|
ServerURL: "https://foobar.docker.io:2376/v1",
|
||||||
Username: "foobar",
|
Username: "foobar",
|
||||||
Password: "foobarbaz",
|
Secret: "foobarbaz",
|
||||||
}
|
}
|
||||||
|
creds1 := &credentials.Credentials{
|
||||||
helper := New()
|
ServerURL: "https://foobar.docker.io:2376/v2",
|
||||||
|
Username: "foobarbaz",
|
||||||
|
Secret: "foobar",
|
||||||
|
}
|
||||||
|
helper := Osxkeychain{}
|
||||||
if err := helper.Add(creds); err != nil {
|
if err := helper.Add(creds); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
username, password, err := helper.Get(creds.ServerURL)
|
username, secret, err := helper.Get(creds.ServerURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -27,8 +30,23 @@ func TestOSXKeychainHelper(t *testing.T) {
|
|||||||
t.Fatalf("expected %s, got %s\n", "foobar", username)
|
t.Fatalf("expected %s, got %s\n", "foobar", username)
|
||||||
}
|
}
|
||||||
|
|
||||||
if password != "foobarbaz" {
|
if secret != "foobarbaz" {
|
||||||
t.Fatalf("expected %s, got %s\n", "foobarbaz", password)
|
t.Fatalf("expected %s, got %s\n", "foobarbaz", secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
auths, err := helper.List()
|
||||||
|
if err != nil || len(auths) == 0 {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
helper.Add(creds1)
|
||||||
|
defer helper.Delete(creds1.ServerURL)
|
||||||
|
newauths, err := helper.List()
|
||||||
|
if len(newauths)-len(auths) != 1 {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Error: len(newauths): %d, len(auths): %d", len(newauths), len(auths))
|
||||||
|
}
|
||||||
|
t.Fatalf("Error: len(newauths): %d, len(auths): %d\n Error= %v", len(newauths), len(auths), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := helper.Delete(creds.ServerURL); err != nil {
|
if err := helper.Delete(creds.ServerURL); err != nil {
|
||||||
@@ -37,9 +55,9 @@ func TestOSXKeychainHelper(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestMissingCredentials(t *testing.T) {
|
func TestMissingCredentials(t *testing.T) {
|
||||||
helper := New()
|
helper := Osxkeychain{}
|
||||||
_, _, err := helper.Get("https://adsfasdf.wrewerwer.com/asdfsdddd")
|
_, _, err := helper.Get("https://adsfasdf.wrewerwer.com/asdfsdddd")
|
||||||
if err != credentials.ErrCredentialsNotFound {
|
if !credentials.IsErrCredentialsNotFound(err) {
|
||||||
t.Fatalf("exptected ErrCredentialsNotFound, got %v", err)
|
t.Fatalf("expected ErrCredentialsNotFound, got %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/docker/docker-credential-helpers/credentials"
|
||||||
|
"github.com/docker/docker-credential-helpers/secretservice"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
credentials.Serve(secretservice.Secretservice{})
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
#include <string.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include "secretservice_linux.h"
|
||||||
|
|
||||||
|
const SecretSchema *docker_get_schema(void)
|
||||||
|
{
|
||||||
|
static const SecretSchema docker_schema = {
|
||||||
|
"io.docker.Credentials", SECRET_SCHEMA_NONE,
|
||||||
|
{
|
||||||
|
{ "server", SECRET_SCHEMA_ATTRIBUTE_STRING },
|
||||||
|
{ "username", SECRET_SCHEMA_ATTRIBUTE_STRING },
|
||||||
|
{ "docker_cli", SECRET_SCHEMA_ATTRIBUTE_STRING },
|
||||||
|
{ "NULL", 0 },
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return &docker_schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
GError *add(char *server, char *username, char *secret) {
|
||||||
|
GError *err = NULL;
|
||||||
|
|
||||||
|
secret_password_store_sync (DOCKER_SCHEMA, SECRET_COLLECTION_DEFAULT,
|
||||||
|
server, secret, NULL, &err,
|
||||||
|
"server", server,
|
||||||
|
"username", username,
|
||||||
|
"docker_cli", "1",
|
||||||
|
NULL);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
GError *delete(char *server) {
|
||||||
|
GError *err = NULL;
|
||||||
|
|
||||||
|
secret_password_clear_sync(DOCKER_SCHEMA, NULL, &err,
|
||||||
|
"server", server,
|
||||||
|
"docker_cli", "1",
|
||||||
|
NULL);
|
||||||
|
if (err != NULL)
|
||||||
|
return err;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
char *get_username(SecretItem *item) {
|
||||||
|
GHashTable *attributes;
|
||||||
|
GHashTableIter iter;
|
||||||
|
gchar *value, *key;
|
||||||
|
|
||||||
|
attributes = secret_item_get_attributes(item);
|
||||||
|
g_hash_table_iter_init(&iter, attributes);
|
||||||
|
while (g_hash_table_iter_next(&iter, (void **)&key, (void **)&value)) {
|
||||||
|
if (strncmp(key, "username", strlen(key)) == 0)
|
||||||
|
return (char *)value;
|
||||||
|
}
|
||||||
|
g_hash_table_unref(attributes);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
GError *get(char *server, char **username, char **secret) {
|
||||||
|
GError *err = NULL;
|
||||||
|
GHashTable *attributes;
|
||||||
|
SecretService *service;
|
||||||
|
GList *items, *l;
|
||||||
|
SecretSearchFlags flags = SECRET_SEARCH_LOAD_SECRETS | SECRET_SEARCH_ALL | SECRET_SEARCH_UNLOCK;
|
||||||
|
SecretValue *secretValue;
|
||||||
|
gsize length;
|
||||||
|
gchar *value;
|
||||||
|
|
||||||
|
attributes = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
|
||||||
|
g_hash_table_insert(attributes, g_strdup("server"), g_strdup(server));
|
||||||
|
g_hash_table_insert(attributes, g_strdup("docker_cli"), g_strdup("1"));
|
||||||
|
|
||||||
|
service = secret_service_get_sync(SECRET_SERVICE_NONE, NULL, &err);
|
||||||
|
if (err == NULL) {
|
||||||
|
items = secret_service_search_sync(service, NULL, attributes, flags, NULL, &err);
|
||||||
|
if (err == NULL) {
|
||||||
|
for (l = items; l != NULL; l = g_list_next(l)) {
|
||||||
|
value = secret_item_get_schema_name(l->data);
|
||||||
|
if (strncmp(value, "io.docker.Credentials", strlen(value)) != 0) {
|
||||||
|
g_free(value);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
g_free(value);
|
||||||
|
secretValue = secret_item_get_secret(l->data);
|
||||||
|
if (secret != NULL) {
|
||||||
|
*secret = strdup(secret_value_get(secretValue, &length));
|
||||||
|
secret_value_unref(secretValue);
|
||||||
|
}
|
||||||
|
*username = get_username(l->data);
|
||||||
|
}
|
||||||
|
g_list_free_full(items, g_object_unref);
|
||||||
|
}
|
||||||
|
g_object_unref(service);
|
||||||
|
}
|
||||||
|
g_hash_table_unref(attributes);
|
||||||
|
if (err != NULL) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
GError *list(char *** paths, char *** accts, unsigned int *list_l) {
|
||||||
|
GList *items;
|
||||||
|
GError *err = NULL;
|
||||||
|
SecretService *service;
|
||||||
|
SecretSearchFlags flags = SECRET_SEARCH_LOAD_SECRETS | SECRET_SEARCH_ALL | SECRET_SEARCH_UNLOCK;
|
||||||
|
GHashTable *attributes;
|
||||||
|
g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
|
||||||
|
attributes = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
|
||||||
|
service = secret_service_get_sync(SECRET_SERVICE_NONE, NULL, &err);
|
||||||
|
items = secret_service_search_sync(service, NULL, attributes, flags, NULL, &err);
|
||||||
|
int numKeys = g_list_length(items);
|
||||||
|
if (err != NULL) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
*paths = (char **) malloc((int)sizeof(char *)*numKeys);
|
||||||
|
*accts = (char **) malloc((int)sizeof(char *)*numKeys);
|
||||||
|
// items now contains our keys from the gnome keyring
|
||||||
|
// we will now put it in our two lists to return it to go
|
||||||
|
GList *current;
|
||||||
|
int listNumber = 0;
|
||||||
|
for(current = items; current!=NULL; current = current->next) {
|
||||||
|
char *pathTmp = secret_item_get_label(current->data);
|
||||||
|
// you cannot have a key without a label in the gnome keyring
|
||||||
|
char *acctTmp = get_username(current->data);
|
||||||
|
if (acctTmp==NULL) {
|
||||||
|
acctTmp = "account not defined";
|
||||||
|
}
|
||||||
|
char *path = (char *) malloc(strlen(pathTmp));
|
||||||
|
char *acct = (char *) malloc(strlen(acctTmp));
|
||||||
|
path = pathTmp;
|
||||||
|
acct = acctTmp;
|
||||||
|
(*paths)[listNumber] = (char *) malloc(sizeof(char)*(strlen(path)));
|
||||||
|
memcpy((*paths)[listNumber], path, sizeof(char)*(strlen(path)));
|
||||||
|
(*accts)[listNumber] = (char *) malloc(sizeof(char)*(strlen(acct)));
|
||||||
|
memcpy((*accts)[listNumber], acct, sizeof(char)*(strlen(acct)));
|
||||||
|
listNumber = listNumber + 1;
|
||||||
|
}
|
||||||
|
*list_l = numKeys;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
void freeListData(char *** data, unsigned int length) {
|
||||||
|
int i;
|
||||||
|
for(i=0; i<length; i++) {
|
||||||
|
free((*data)[i]);
|
||||||
|
}
|
||||||
|
free(*data);
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package secretservice
|
||||||
|
|
||||||
|
/*
|
||||||
|
#cgo pkg-config: libsecret-1
|
||||||
|
|
||||||
|
#include "secretservice_linux.h"
|
||||||
|
#include <stdlib.h>
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/docker/docker-credential-helpers/credentials"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Secretservice handles secrets using Linux secret-service as a store.
|
||||||
|
type Secretservice struct{}
|
||||||
|
|
||||||
|
// Add adds new credentials to the keychain.
|
||||||
|
func (h Secretservice) Add(creds *credentials.Credentials) error {
|
||||||
|
if creds == nil {
|
||||||
|
return errors.New("missing credentials")
|
||||||
|
}
|
||||||
|
server := C.CString(creds.ServerURL)
|
||||||
|
defer C.free(unsafe.Pointer(server))
|
||||||
|
username := C.CString(creds.Username)
|
||||||
|
defer C.free(unsafe.Pointer(username))
|
||||||
|
secret := C.CString(creds.Secret)
|
||||||
|
defer C.free(unsafe.Pointer(secret))
|
||||||
|
|
||||||
|
if err := C.add(server, username, secret); err != nil {
|
||||||
|
defer C.g_error_free(err)
|
||||||
|
errMsg := (*C.char)(unsafe.Pointer(err.message))
|
||||||
|
return errors.New(C.GoString(errMsg))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete removes credentials from the store.
|
||||||
|
func (h Secretservice) Delete(serverURL string) error {
|
||||||
|
if serverURL == "" {
|
||||||
|
return errors.New("missing server url")
|
||||||
|
}
|
||||||
|
server := C.CString(serverURL)
|
||||||
|
defer C.free(unsafe.Pointer(server))
|
||||||
|
|
||||||
|
if err := C.delete(server); err != nil {
|
||||||
|
defer C.g_error_free(err)
|
||||||
|
errMsg := (*C.char)(unsafe.Pointer(err.message))
|
||||||
|
return errors.New(C.GoString(errMsg))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns the username and secret to use for a given registry server URL.
|
||||||
|
func (h Secretservice) Get(serverURL string) (string, string, error) {
|
||||||
|
if serverURL == "" {
|
||||||
|
return "", "", errors.New("missing server url")
|
||||||
|
}
|
||||||
|
var username *C.char
|
||||||
|
defer C.free(unsafe.Pointer(username))
|
||||||
|
var secret *C.char
|
||||||
|
defer C.free(unsafe.Pointer(secret))
|
||||||
|
server := C.CString(serverURL)
|
||||||
|
defer C.free(unsafe.Pointer(server))
|
||||||
|
|
||||||
|
err := C.get(server, &username, &secret)
|
||||||
|
if err != nil {
|
||||||
|
defer C.g_error_free(err)
|
||||||
|
errMsg := (*C.char)(unsafe.Pointer(err.message))
|
||||||
|
return "", "", errors.New(C.GoString(errMsg))
|
||||||
|
}
|
||||||
|
user := C.GoString(username)
|
||||||
|
pass := C.GoString(secret)
|
||||||
|
if pass == "" {
|
||||||
|
return "", "", credentials.NewErrCredentialsNotFound()
|
||||||
|
}
|
||||||
|
return user, pass, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// List returns the stored URLs and corresponding usernames.
|
||||||
|
func (h Secretservice) List() (map[string]string, error) {
|
||||||
|
var pathsC **C.char
|
||||||
|
defer C.free(unsafe.Pointer(pathsC))
|
||||||
|
var acctsC **C.char
|
||||||
|
defer C.free(unsafe.Pointer(acctsC))
|
||||||
|
var listLenC C.uint
|
||||||
|
err := C.list(&pathsC, &acctsC, &listLenC)
|
||||||
|
if err != nil {
|
||||||
|
defer C.free(unsafe.Pointer(err))
|
||||||
|
return nil, errors.New("Error from list function in secretservice_linux.c likely due to error in secretservice library")
|
||||||
|
}
|
||||||
|
defer C.freeListData(&pathsC, listLenC)
|
||||||
|
defer C.freeListData(&acctsC, listLenC)
|
||||||
|
|
||||||
|
listLen := int(listLenC)
|
||||||
|
pathTmp := (*[1 << 30]*C.char)(unsafe.Pointer(pathsC))[:listLen:listLen]
|
||||||
|
acctTmp := (*[1 << 30]*C.char)(unsafe.Pointer(acctsC))[:listLen:listLen]
|
||||||
|
resp := make(map[string]string)
|
||||||
|
for i := 0; i < listLen; i++ {
|
||||||
|
resp[C.GoString(pathTmp[i])] = C.GoString(acctTmp[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#define SECRET_WITH_UNSTABLE 1
|
||||||
|
#define SECRET_API_SUBJECT_TO_CHANGE 1
|
||||||
|
#include <libsecret/secret.h>
|
||||||
|
|
||||||
|
const SecretSchema *docker_get_schema(void) G_GNUC_CONST;
|
||||||
|
|
||||||
|
#define DOCKER_SCHEMA docker_get_schema()
|
||||||
|
|
||||||
|
GError *add(char *server, char *username, char *secret);
|
||||||
|
GError *delete(char *server);
|
||||||
|
GError *get(char *server, char **username, char **secret);
|
||||||
|
GError *list(char *** paths, char *** accts, unsigned int *list_l);
|
||||||
|
void freeListData(char *** data, unsigned int length);
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package secretservice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/docker-credential-helpers/credentials"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSecretServiceHelper(t *testing.T) {
|
||||||
|
t.Skip("test requires gnome-keyring but travis CI doesn't have it")
|
||||||
|
|
||||||
|
creds := &credentials.Credentials{
|
||||||
|
ServerURL: "https://foobar.docker.io:2376/v1",
|
||||||
|
Username: "foobar",
|
||||||
|
Secret: "foobarbaz",
|
||||||
|
}
|
||||||
|
|
||||||
|
helper := Secretservice{}
|
||||||
|
if err := helper.Add(creds); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
username, secret, err := helper.Get(creds.ServerURL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if username != "foobar" {
|
||||||
|
t.Fatalf("expected %s, got %s\n", "foobar", username)
|
||||||
|
}
|
||||||
|
|
||||||
|
if secret != "foobarbaz" {
|
||||||
|
t.Fatalf("expected %s, got %s\n", "foobarbaz", secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := helper.Delete(creds.ServerURL); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
auths, err := helper.List()
|
||||||
|
if err != nil || len(auths) == 0 {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
helper.Add(creds)
|
||||||
|
if newauths, err := helper.List(); (len(newauths) - len(auths)) != 1 {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMissingCredentials(t *testing.T) {
|
||||||
|
t.Skip("test requires gnome-keyring but travis CI doesn't have it")
|
||||||
|
|
||||||
|
helper := Secretservice{}
|
||||||
|
_, _, err := helper.Get("https://adsfasdf.wrewerwer.com/asdfsdddd")
|
||||||
|
if !credentials.IsErrCredentialsNotFound(err) {
|
||||||
|
t.Fatalf("expected ErrCredentialsNotFound, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
-4
@@ -1,7 +1,6 @@
|
|||||||
package wincred
|
package wincred
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"C"
|
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"reflect"
|
"reflect"
|
||||||
"syscall"
|
"syscall"
|
||||||
@@ -10,6 +9,8 @@ import (
|
|||||||
"unsafe"
|
"unsafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var nullPointer = unsafe.Pointer(uintptr(0))
|
||||||
|
|
||||||
// Create a Go string using a pointer to a zero-terminated UTF 16 encoded string.
|
// Create a Go string using a pointer to a zero-terminated UTF 16 encoded string.
|
||||||
// See github.com/AllenDang/w32
|
// See github.com/AllenDang/w32
|
||||||
func utf16PtrToString(wstr *uint16) string {
|
func utf16PtrToString(wstr *uint16) string {
|
||||||
@@ -36,8 +37,22 @@ func utf16ToByte(wstr []uint16) (result []byte) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Copies the given C byte array to a Go byte array (see `C.GoBytes`)
|
||||||
|
func goBytes(src unsafe.Pointer, len uint32) []byte {
|
||||||
|
if src == nullPointer {
|
||||||
|
return []byte{}
|
||||||
|
}
|
||||||
|
slice := (*[1 << 30]byte)(src)[0:len]
|
||||||
|
rv := make([]byte, len)
|
||||||
|
copy(rv, slice)
|
||||||
|
return rv[:]
|
||||||
|
}
|
||||||
|
|
||||||
// Convert the given CREDENTIAL struct to a more usable structure
|
// Convert the given CREDENTIAL struct to a more usable structure
|
||||||
func nativeToCredential(cred *nativeCREDENTIAL) (result *Credential) {
|
func nativeToCredential(cred *nativeCREDENTIAL) (result *Credential) {
|
||||||
|
if unsafe.Pointer(cred) == nullPointer {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
result = new(Credential)
|
result = new(Credential)
|
||||||
result.Comment = utf16PtrToString(cred.Comment)
|
result.Comment = utf16PtrToString(cred.Comment)
|
||||||
result.TargetName = utf16PtrToString(cred.TargetName)
|
result.TargetName = utf16PtrToString(cred.TargetName)
|
||||||
@@ -45,7 +60,7 @@ func nativeToCredential(cred *nativeCREDENTIAL) (result *Credential) {
|
|||||||
result.UserName = utf16PtrToString(cred.UserName)
|
result.UserName = utf16PtrToString(cred.UserName)
|
||||||
result.LastWritten = time.Unix(0, cred.LastWritten.Nanoseconds())
|
result.LastWritten = time.Unix(0, cred.LastWritten.Nanoseconds())
|
||||||
result.Persist = CredentialPersistence(cred.Persist)
|
result.Persist = CredentialPersistence(cred.Persist)
|
||||||
result.CredentialBlob = C.GoBytes(unsafe.Pointer(cred.CredentialBlob), C.int(cred.CredentialBlobSize))
|
result.CredentialBlob = goBytes(unsafe.Pointer(cred.CredentialBlob), cred.CredentialBlobSize)
|
||||||
result.Attributes = make([]CredentialAttribute, cred.AttributeCount)
|
result.Attributes = make([]CredentialAttribute, cred.AttributeCount)
|
||||||
attrSliceHeader := reflect.SliceHeader{
|
attrSliceHeader := reflect.SliceHeader{
|
||||||
Data: cred.Attributes,
|
Data: cred.Attributes,
|
||||||
@@ -56,15 +71,17 @@ func nativeToCredential(cred *nativeCREDENTIAL) (result *Credential) {
|
|||||||
for i, attr := range attrSlice {
|
for i, attr := range attrSlice {
|
||||||
resultAttr := &result.Attributes[i]
|
resultAttr := &result.Attributes[i]
|
||||||
resultAttr.Keyword = utf16PtrToString(attr.Keyword)
|
resultAttr.Keyword = utf16PtrToString(attr.Keyword)
|
||||||
resultAttr.Value = C.GoBytes(unsafe.Pointer(attr.Value), C.int(attr.ValueSize))
|
resultAttr.Value = goBytes(unsafe.Pointer(attr.Value), attr.ValueSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert the given Credential object back to a CREDENTIAL struct, which can be used for calling the
|
// Convert the given Credential object back to a CREDENTIAL struct, which can be used for calling the
|
||||||
// Windows APIs
|
// Windows APIs
|
||||||
func nativeFromCredential(cred *Credential) (result *nativeCREDENTIAL) {
|
func nativeFromCredential(cred *Credential) (result *nativeCREDENTIAL) {
|
||||||
|
if cred == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
result = new(nativeCREDENTIAL)
|
result = new(nativeCREDENTIAL)
|
||||||
result.Flags = 0
|
result.Flags = 0
|
||||||
result.Type = 0
|
result.Type = 0
|
||||||
|
|||||||
+42
-4
@@ -8,12 +8,18 @@ import (
|
|||||||
var (
|
var (
|
||||||
modadvapi32 = syscall.NewLazyDLL("advapi32.dll")
|
modadvapi32 = syscall.NewLazyDLL("advapi32.dll")
|
||||||
|
|
||||||
procCredRead = modadvapi32.NewProc("CredReadW")
|
procCredRead proc = modadvapi32.NewProc("CredReadW")
|
||||||
procCredWrite = modadvapi32.NewProc("CredWriteW")
|
procCredWrite proc = modadvapi32.NewProc("CredWriteW")
|
||||||
procCredDelete = modadvapi32.NewProc("CredDeleteW")
|
procCredDelete proc = modadvapi32.NewProc("CredDeleteW")
|
||||||
procCredFree = modadvapi32.NewProc("CredFree")
|
procCredFree proc = modadvapi32.NewProc("CredFree")
|
||||||
|
procCredEnumerate proc = modadvapi32.NewProc("CredEnumerateW")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Interface for syscall.Proc: helps testing
|
||||||
|
type proc interface {
|
||||||
|
Call(a ...uintptr) (r1, r2 uintptr, lastErr error)
|
||||||
|
}
|
||||||
|
|
||||||
// http://msdn.microsoft.com/en-us/library/windows/desktop/aa374788(v=vs.85).aspx
|
// http://msdn.microsoft.com/en-us/library/windows/desktop/aa374788(v=vs.85).aspx
|
||||||
type nativeCREDENTIAL struct {
|
type nativeCREDENTIAL struct {
|
||||||
Flags uint32
|
Flags uint32
|
||||||
@@ -48,6 +54,8 @@ const (
|
|||||||
naCRED_TYPE_DOMAIN_VISIBLE_PASSWORD nativeCRED_TYPE = 0x4
|
naCRED_TYPE_DOMAIN_VISIBLE_PASSWORD nativeCRED_TYPE = 0x4
|
||||||
naCRED_TYPE_GENERIC_CERTIFICATE nativeCRED_TYPE = 0x5
|
naCRED_TYPE_GENERIC_CERTIFICATE nativeCRED_TYPE = 0x5
|
||||||
naCRED_TYPE_DOMAIN_EXTENDED nativeCRED_TYPE = 0x6
|
naCRED_TYPE_DOMAIN_EXTENDED nativeCRED_TYPE = 0x6
|
||||||
|
|
||||||
|
naERROR_NOT_FOUND = "Element not found."
|
||||||
)
|
)
|
||||||
|
|
||||||
// http://msdn.microsoft.com/en-us/library/windows/desktop/aa374804(v=vs.85).aspx
|
// http://msdn.microsoft.com/en-us/library/windows/desktop/aa374804(v=vs.85).aspx
|
||||||
@@ -97,3 +105,33 @@ func nativeCredDelete(cred *Credential, typ nativeCRED_TYPE) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa374794(v=vs.85).aspx
|
||||||
|
func nativeCredEnumerate(filter string, all bool) ([]*Credential, error) {
|
||||||
|
var count int
|
||||||
|
var pcreds uintptr
|
||||||
|
var filterPtr uintptr
|
||||||
|
if !all {
|
||||||
|
filterUtf16Ptr, _ := syscall.UTF16PtrFromString(filter)
|
||||||
|
filterPtr = uintptr(unsafe.Pointer(filterUtf16Ptr))
|
||||||
|
} else {
|
||||||
|
filterPtr = 0
|
||||||
|
}
|
||||||
|
ret, _, err := procCredEnumerate.Call(
|
||||||
|
filterPtr,
|
||||||
|
0,
|
||||||
|
uintptr(unsafe.Pointer(&count)),
|
||||||
|
uintptr(unsafe.Pointer(&pcreds)),
|
||||||
|
)
|
||||||
|
if ret == 0 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer procCredFree.Call(pcreds)
|
||||||
|
pcredsSlice := (*[1 << 30]uintptr)(unsafe.Pointer(pcreds))[:count:count]
|
||||||
|
creds := make([]*Credential, count)
|
||||||
|
for i := range creds {
|
||||||
|
creds[i] = nativeToCredential((*nativeCREDENTIAL)(unsafe.Pointer(pcredsSlice[i])))
|
||||||
|
}
|
||||||
|
|
||||||
|
return creds, nil
|
||||||
|
}
|
||||||
|
|||||||
+11
@@ -67,3 +67,14 @@ func (t *DomainPassword) Delete() (err error) {
|
|||||||
func (t *DomainPassword) SetPassword(pw string) {
|
func (t *DomainPassword) SetPassword(pw string) {
|
||||||
t.CredentialBlob = utf16ToByte(syscall.StringToUTF16(pw))
|
t.CredentialBlob = utf16ToByte(syscall.StringToUTF16(pw))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// List the contents of the Credentials store
|
||||||
|
func List() ([]*Credential, error) {
|
||||||
|
creds, err := nativeCredEnumerate("", true)
|
||||||
|
if err != nil && err.Error() == naERROR_NOT_FOUND {
|
||||||
|
// Ignore ERROR_NOT_FOUND and return an empty list instead
|
||||||
|
creds = []*Credential{}
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
return creds, err
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,5 +6,5 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
credentials.Serve(wincred.New())
|
credentials.Serve(wincred.Wincred{})
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-11
@@ -5,24 +5,20 @@ import (
|
|||||||
"github.com/docker/docker-credential-helpers/credentials"
|
"github.com/docker/docker-credential-helpers/credentials"
|
||||||
)
|
)
|
||||||
|
|
||||||
type wincred struct{}
|
// Wincred handles secrets using the Windows credential service.
|
||||||
|
type Wincred struct{}
|
||||||
// New creates a new wincred.
|
|
||||||
func New() credentials.Helper {
|
|
||||||
return wincred{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add adds new credentials to the windows credentials manager.
|
// Add adds new credentials to the windows credentials manager.
|
||||||
func (h wincred) Add(creds *credentials.Credentials) error {
|
func (h Wincred) Add(creds *credentials.Credentials) error {
|
||||||
g := winc.NewGenericCredential(creds.ServerURL)
|
g := winc.NewGenericCredential(creds.ServerURL)
|
||||||
g.UserName = creds.Username
|
g.UserName = creds.Username
|
||||||
g.CredentialBlob = []byte(creds.Password)
|
g.CredentialBlob = []byte(creds.Secret)
|
||||||
g.Persist = winc.PersistLocalMachine
|
g.Persist = winc.PersistLocalMachine
|
||||||
return g.Write()
|
return g.Write()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete removes credentials from the windows credentials manager.
|
// Delete removes credentials from the windows credentials manager.
|
||||||
func (h wincred) Delete(serverURL string) error {
|
func (h Wincred) Delete(serverURL string) error {
|
||||||
g, err := winc.GetGenericCredential(serverURL)
|
g, err := winc.GetGenericCredential(serverURL)
|
||||||
if g == nil {
|
if g == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -34,10 +30,24 @@ func (h wincred) Delete(serverURL string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get retrieves credentials from the windows credentials manager.
|
// Get retrieves credentials from the windows credentials manager.
|
||||||
func (h wincred) Get(serverURL string) (string, string, error) {
|
func (h Wincred) Get(serverURL string) (string, string, error) {
|
||||||
g, _ := winc.GetGenericCredential(serverURL)
|
g, _ := winc.GetGenericCredential(serverURL)
|
||||||
if g == nil {
|
if g == nil {
|
||||||
return "", "", credentials.ErrCredentialsNotFound
|
return "", "", credentials.NewErrCredentialsNotFound()
|
||||||
}
|
}
|
||||||
return g.UserName, string(g.CredentialBlob), nil
|
return g.UserName, string(g.CredentialBlob), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// List returns the stored URLs and corresponding usernames.
|
||||||
|
func (h Wincred) List() (map[string]string, error) {
|
||||||
|
creds, err := winc.List()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := make(map[string]string)
|
||||||
|
for i := range creds {
|
||||||
|
resp[creds[i].TargetName] = creds[i].UserName
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,15 +10,20 @@ func TestWinCredHelper(t *testing.T) {
|
|||||||
creds := &credentials.Credentials{
|
creds := &credentials.Credentials{
|
||||||
ServerURL: "https://foobar.docker.io:2376/v1",
|
ServerURL: "https://foobar.docker.io:2376/v1",
|
||||||
Username: "foobar",
|
Username: "foobar",
|
||||||
Password: "foobarbaz",
|
Secret: "foobarbaz",
|
||||||
|
}
|
||||||
|
creds1 := &credentials.Credentials{
|
||||||
|
ServerURL: "https://foobar.docker.io:2376/v2",
|
||||||
|
Username: "foobarbaz",
|
||||||
|
Secret: "foobar",
|
||||||
}
|
}
|
||||||
|
|
||||||
helper := New()
|
helper := Wincred{}
|
||||||
if err := helper.Add(creds); err != nil {
|
if err := helper.Add(creds); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
username, password, err := helper.Get(creds.ServerURL)
|
username, secret, err := helper.Get(creds.ServerURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -27,8 +32,27 @@ func TestWinCredHelper(t *testing.T) {
|
|||||||
t.Fatalf("expected %s, got %s\n", "foobar", username)
|
t.Fatalf("expected %s, got %s\n", "foobar", username)
|
||||||
}
|
}
|
||||||
|
|
||||||
if password != "foobarbaz" {
|
if secret != "foobarbaz" {
|
||||||
t.Fatalf("expected %s, got %s\n", "foobarbaz", password)
|
t.Fatalf("expected %s, got %s\n", "foobarbaz", secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
auths, err := helper.List()
|
||||||
|
if err != nil || len(auths) == 0 {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
helper.Add(creds1)
|
||||||
|
defer helper.Delete(creds1.ServerURL)
|
||||||
|
newauths, err := helper.List()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(newauths)-len(auths) != 1 {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Error: len(newauths): %d, len(auths): %d", len(newauths), len(auths))
|
||||||
|
}
|
||||||
|
t.Fatalf("Error: len(newauths): %d, len(auths): %d\n Error= %v", len(newauths), len(auths), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := helper.Delete(creds.ServerURL); err != nil {
|
if err := helper.Delete(creds.ServerURL); err != nil {
|
||||||
@@ -37,9 +61,9 @@ func TestWinCredHelper(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestMissingCredentials(t *testing.T) {
|
func TestMissingCredentials(t *testing.T) {
|
||||||
helper := New()
|
helper := Wincred{}
|
||||||
_, _, err := helper.Get("https://adsfasdf.wrewerwer.com/asdfsdddd")
|
_, _, err := helper.Get("https://adsfasdf.wrewerwer.com/asdfsdddd")
|
||||||
if err != credentials.ErrCredentialsNotFound {
|
if !credentials.IsErrCredentialsNotFound(err) {
|
||||||
t.Fatalf("exptected ErrCredentialsNotFound, got %v", err)
|
t.Fatalf("expected ErrCredentialsNotFound, got %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user