1
0
mirror of https://github.com/docker/docker-credential-helpers.git synced 2026-06-28 07:11:36 +05:30

Compare commits

..

85 Commits

Author SHA1 Message Date
Jean-Laurent de Morlhon ce617b3357 Merge pull request #52 from jeanlaurent/run-test-on-release
Clean and run test before osx release
2017-03-13 16:45:02 -07:00
Jean-Laurent de Morlhon f3071aff0a Merge pull request #53 from n4ss/fix-windows-typos-tests
Fix syntax typos and tests implementation for Windows
2017-03-13 16:44:48 -07:00
Nassim 'Nass' Eddequiouaq 1515d4547e Fix syntax typos and tests implementation for windows
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2017-03-13 14:51:03 -07:00
Jean-Laurent de Morlhon f67589c36e Clean and run test before osx release
Signed-off-by: Jean-Laurent de Morlhon <jeanlaurent@morlhon.net>
2017-03-13 12:49:05 -07:00
Vincent Demeester 479de2a4f5 Merge pull request #45 from jeanlaurent/fix-mkdir
Remove mkdir -p where neccessary
2017-03-13 20:43:59 +01:00
Vincent Demeester cdba2ced06 Merge pull request #44 from jeanlaurent/osxrelease-task
Add a osx release target to the makefile to ease releasing.
2017-03-13 20:43:49 +01:00
Nassim Eddequiouaq 7f0538cd5e Merge pull request #50 from n4ss/fix-secserv-tests
Fix secretservice tests and expected behaviors
2017-03-13 11:56:46 -07:00
Nassim Eddequiouaq 14381bf0d6 Merge pull request #49 from n4ss/label-creds
Label credentials on each platform' creds store and fix secretservice behavior
2017-03-13 11:22:23 -07:00
Nassim 'Nass' Eddequiouaq 7133af577e Creds label can be configured from helpers
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2017-03-11 12:34:22 +01:00
Nassim 'Nass' Eddequiouaq 2f2e85cfb9 Remove unnecessary creds list's size check
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2017-03-10 16:59:57 +01:00
Nassim 'Nass' Eddequiouaq 47566329ff Fix secretservice tests and expected behaviors
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2017-03-10 07:29:25 -08:00
Nassim 'Nass' Eddequiouaq b9d19b479a Return empty server-url to usernames map if no search results
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2017-03-10 15:23:43 +01:00
Nassim 'Nass' Eddequiouaq e522e56699 Fix memory leaks and non-null terminated strings usage
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2017-03-10 15:23:17 +01:00
Nassim 'Nass' Eddequiouaq 8cb3338668 Filter docker credentials with label directly through libsecret
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2017-03-10 15:18:14 +01:00
Nassim 'Nass' Eddequiouaq cd76e4253f Use the proper docker secret schema for items to search for
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2017-03-10 15:15:05 +01:00
Nassim 'Nass' Eddequiouaq 021d7d6a19 Add label filter on the list of secrets
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2017-03-10 15:14:34 +01:00
Nassim 'Nass' Eddequiouaq 2a8670e0da Cleanup original modifications to the exposed APIs
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2017-03-10 15:14:04 +01:00
Nassim 'Nass' Eddequiouaq c5fbd3a5ad Fix type conversion for labels added to search queries on macOS
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2017-03-10 15:12:53 +01:00
Nassim 'Nass' Eddequiouaq c6cf8aa13b Add Label to Credentials in test files
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2017-03-10 15:12:32 +01:00
Nassim 'Nass' Eddequiouaq cfe7556d6d [SYNTAX] Run gofmt on changed files
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2017-03-10 15:12:12 +01:00
Nassim 'Nass' Eddequiouaq 23a1f310a5 Add a Docker Credentials label support for windows
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2017-03-10 15:11:57 +01:00
Nassim 'Nass' Eddequiouaq f7f2744e6d Add a Docker Credentials label support for linux
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2017-03-10 15:11:25 +01:00
Nassim 'Nass' Eddequiouaq 406812bf8e Add a Docker Credentials label support for macOS
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2017-03-10 15:11:13 +01:00
Nassim 'Nass' Eddequiouaq 595b7f2531 Add a Docker Credentials label to store and list creds
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2017-03-10 15:10:54 +01:00
Jean-Laurent de Morlhon ad4463616e Remove mkdir -p where neccessary
Signed-off-by: Jean-Laurent de Morlhon <jeanlaurent@morlhon.net>
2017-03-03 18:51:21 +01:00
Jean-Laurent de Morlhon 365da011fb Add a osx release target to the makefile to ease releasing.
Signed-off-by: Jean-Laurent de Morlhon <jeanlaurent@morlhon.net>
2017-03-03 18:49:23 +01:00
Vincent Demeester 1057cf7f86 Merge pull request #43 from jeanlaurent/windows-makefile
Add .exe to windows Makefile target
2017-03-03 18:03:14 +01:00
Jean-Laurent de Morlhon 40d06d0090 Add .exe to windows Makefile target
Signed-off-by: Jean-Laurent de Morlhon <jeanlaurent@morlhon.net>
2017-03-03 17:58:06 +01:00
Vincent Demeester de50f50ab0 Merge pull request #41 from jeanlaurent/codesign
Add a makefile target to codesign output binary on macOS
2017-03-03 17:56:55 +01:00
Vincent Demeester 3c3e1d3af1 Merge pull request #42 from ebriney/fix-keychain-list
Reconstruct the full url when doing `keychain_list`
2017-03-03 17:56:15 +01:00
Emmanuel Briney 19ec1c3164 reconstruct the full url when doing keychain_list
Signed-off-by: Emmanuel Briney <emmanuel.briney@docker.com>
2017-03-03 17:16:00 +01:00
Jean-Laurent de Morlhon 2a3f7a4468 Add a makefile target to codesign output binary on macOS
Signed-off-by: Jean-Laurent de Morlhon <jeanlaurent@morlhon.net>
2017-03-03 16:20:29 +01:00
Vincent Demeester b7c53e02cd Merge pull request #40 from ebriney/osx10.10-10.11-compatibility
Add cgo flags to support older version of OSX (10.10, 10.11)
2017-02-21 10:55:47 +01:00
Emmanuel Briney 94963d0da8 osxkeychain: add cgo flags to support older version of OSX
Signed-off-by: Emmanuel Briney <emmanuel.briney@docker.com>
2017-02-21 10:41:54 +01:00
Vincent Demeester 2514d5e8b2 Merge pull request #39 from docker/release-v0.4.0
Prepare v0.4.0 release
2017-02-16 15:55:09 +01:00
Vincent Demeester 01ed4b811b Merge pull request #26 from danieljoos/master
Updated wincred vendor & fixed Windows build
2017-02-16 15:52:37 +01:00
Vincent Demeester 9b6be7c243 Merge pull request #28 from KingEmet/patch-1
Update README.md
2017-02-16 11:41:56 +01:00
Vincent Demeester f853612e07 Prepare v0.4.0 release
Signed-off-by: Vincent Demeester <vincent@sbr.pm>
2017-02-16 11:19:45 +01:00
Brian Goff 69c9d2eab8 Merge pull request #38 from ebriney/mac-delete-before-adding
mac: delete credentials before adding them
2017-02-15 08:44:08 -05:00
ebriney eecc09c974 mac: delete credentials before adding them to avoid already exist error (fixes #37)
Signed-off-by: ebriney <emmanuel.briney@docker.com>
2017-02-14 22:50:44 +01:00
KingEmet 48079a964a add 'list' to API list
Signed-off-by: KingEmet <jsand@google.com>
2016-09-07 15:59:21 -07:00
Daniel Joos cfbce1c845 Removed unnecessary lines in wincred_windows.go
Those lines seem to be left from the last merge.
They cause build errors on Windows.

Signed-off-by: Daniel Joos <daniel@joosweb.de>
2016-09-07 09:59:03 +02:00
Daniel Joos a994ca1d54 Updated vendor pkg: github.com/danieljoos/wincred
This includes the following changes:
- Removed need for `C`
- Added some null checks to avoid possible panics
- `List` returns an empty list instead of an error, in case no credentials
  are installed on the system

Signed-off-by: Daniel Joos <daniel@joosweb.de>
2016-09-07 09:58:33 +02:00
Antonio Murdaca f72c04f1d8 Merge pull request #25 from KingEmet/master
Implement client.List, change list API
2016-09-06 18:43:16 +02:00
Jake Sanders c45d9e9e28 Implement client.List, change list API
[]string, []string -> map[string]string because the other APIs assume a
1:1 correspondence

Signed-off-by: Jake Sanders <jsand@google.com>
2016-09-02 15:15:12 -07:00
David Calavera 80833adff5 Merge pull request #18 from avaid96/listing
Implementing listing functionality across OSX, Linux and Windows
2016-07-20 13:55:38 -07:00
avaid96 f1498a0524 go fmt is all osx files and general files
Signed-off-by: avaid96 <avaid1996@gmail.com>
2016-07-16 12:19:47 -07:00
Avi Vaid c2abee4c0c go fmt in windows files
Signed-off-by: Avi Vaid <avaid1996@gmail.com>
2016-07-16 12:18:03 -07:00
avaid96 9e96a4905d fixed issue with wincred
Signed-off-by: avaid96 <avaid1996@gmail.com>
2016-07-16 12:09:31 -07:00
avaid96 cf9b6df432 fixed issue with wincred
Signed-off-by: avaid96 <avaid1996@gmail.com>
2016-07-16 12:08:55 -07:00
avaid96 b0c64357eb fixed issue with the windows test
Signed-off-by: avaid96 <avaid1996@gmail.com>
2016-07-16 12:07:50 -07:00
avaid96 5b764cc13a changes to wincred, completes list functionality in windows
Signed-off-by: avaid96 <avaid1996@gmail.com>
2016-07-16 11:59:43 -07:00
avaid96 b63a32e7a2 fixed another bug in the test for osx
Signed-off-by: avaid96 <avaid1996@gmail.com>
2016-07-15 17:46:46 -07:00
avaid96 173fe2dbc2 fixed a bug in the test for osx
Signed-off-by: avaid96 <avaid1996@gmail.com>
2016-07-15 17:43:52 -07:00
avaid96 b3ebaa455d trying to the OSX test to pass on Travis, it passes locally
Signed-off-by: avaid96 <avaid1996@gmail.com>
2016-07-15 17:32:47 -07:00
avaid96 029e094488 removed a semicolon and the placeholder for the error which we used to locate it. Seems like SecItemCopyMatching is giving an error for some reason- no error on local machine
Signed-off-by: avaid96 <avaid1996@gmail.com>
2016-07-13 14:36:14 -07:00
avaid96 8fa18eb16c implementation on client side as well, complete with tests
Signed-off-by: avaid96 <avaid1996@gmail.com>
2016-07-13 10:20:51 -07:00
avaid96 887a66459a I think OSX tests were failing because it was a list against an empty keychain, checking whether that is the case by filling it first
Signed-off-by: avaid96 <avaid1996@gmail.com>
2016-07-12 10:52:06 -07:00
Avi Vaid d2e6ed779a moved loop variable declaration to comply with c mode in travis
Signed-off-by: Avi Vaid <avaid1996@gmail.com>
2016-07-12 12:51:30 -04:00
avaid96 9557dc3c3f fixing a vet issue
Signed-off-by: avaid96 <avaid1996@gmail.com>
2016-07-12 00:04:34 -07:00
Avi Vaid 59b3d54595 added better test for list in linux
Signed-off-by: Avi Vaid <avaid1996@gmail.com>
2016-07-12 02:52:44 -04:00
avaid96 205e3b3056 cleaned up some of the osx code, added a better test for list
Signed-off-by: avaid96 <avaid1996@gmail.com>
2016-07-11 23:34:20 -07:00
Avi Vaid 7566a1e399 implemented freeing memory in secretservice and made minor edits to osxkeychain
Signed-off-by: Avi Vaid <avaid1996@gmail.com>
2016-07-11 19:45:37 -04:00
Avi Vaid 72661b3103 Implemented list functionality for secretservice- linux
Signed-off-by: Avi Vaid <avaid1996@gmail.com>
2016-07-11 19:33:35 -04:00
avaid96 5a8fb214ed Full implementation for OSX ready
Signed-off-by: avaid96 <avaid1996@gmail.com>
2016-07-11 10:50:44 -07:00
David Calavera 5128fa1bad Release v0.3.0.
Signed-off-by: David Calavera <david.calavera@gmail.com>
2016-05-31 17:41:15 -07:00
Vincent Demeester fee9277aa7 Merge pull request #15 from calavera/client
Add client functions to allow integrations within other CLIs.
2016-06-01 00:08:41 +02:00
David Calavera d80b179703 Merge pull request #16 from calavera/maintainers_file
Add maintainers file.
2016-05-31 14:52:43 -07:00
David Calavera c4fc9c07dd Add client functions to allow integrations within other CLIs.
This is a simplification of how the docker engine implements
this feature, but it will be ported there once this is merged.

Signed-off-by: David Calavera <david.calavera@gmail.com>
2016-05-30 12:32:26 -07:00
David Calavera 2c63e775b4 Add maintainers file.
Signed-off-by: David Calavera <david.calavera@gmail.com>
2016-05-27 18:19:08 -07:00
David Calavera 00703eb6db Merge pull request #13 from calavera/export_helpers
Publish helper structs and methods.
2016-03-24 15:56:00 -07:00
David Calavera df8c7a02f1 Publish helper structs and methods.
Signed-off-by: David Calavera <david.calavera@gmail.com>
2016-03-24 15:08:17 -07:00
David Calavera 4b8917b1cf Merge pull request #12 from calavera/export_library
Export credentials functions to be able to use them as a library.
2016-03-24 13:32:25 -07:00
David Calavera 039c315f22 Export credentials functions to be able to use them as a library.
Signed-off-by: David Calavera <david.calavera@gmail.com>
2016-03-24 14:52:17 -04:00
David Calavera 6bbb56ae3c Merge pull request #11 from mgreau/fix-doc
Update related to the move to Docker Organization
2016-03-18 13:21:15 -07:00
Maxime Gréau ec937eebe2 Update related to the move to Docker Organization
- Fix link to project releases
  - Fix go command

Signed-off-by: Maxime Gréau <greaumaxime@gmail.com>
2016-03-17 00:18:01 +01:00
David Calavera fa24bb8912 Merge pull request #10 from calavera/release_description
Add a link to the Changelog in each release.
2016-03-15 10:27:01 -07:00
David Calavera 76055c2deb Add a link to the Changelog in each release.
Signed-off-by: David Calavera <david.calavera@gmail.com>
2016-03-14 14:04:17 -04:00
David Calavera e68b300c17 Release v0.2.0.
Signed-off-by: David Calavera <david.calavera@gmail.com>
2016-03-14 12:30:40 -04:00
David Calavera 7d02ef740b Merge pull request #8 from calavera/secret_name
Move away from password as a name. We store secrets.
2016-03-14 09:27:50 -07:00
David Calavera bcc242e1ad Merge pull request #9 from calavera/linux_release
Release Linux secret service binary.
2016-03-09 16:26:30 -08:00
David Calavera 8727ffc77b Release Linux secret service binary.
Signed-off-by: David Calavera <david.calavera@gmail.com>
2016-03-09 19:18:24 -05:00
David Calavera 2275377a31 Move away from password as a name. We store secrets.
Signed-off-by: David Calavera <david.calavera@gmail.com>
2016-03-09 16:16:01 -05:00
David Calavera 0fb2225199 Merge pull request #7 from runcom/secretservice
secretservice: add D-Bus secret service
2016-03-07 09:52:48 -08:00
Antonio Murdaca a96948acb3 secretservice: add D-Bus secret service
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
2016-03-07 15:03:35 +01:00
32 changed files with 1526 additions and 141 deletions
+1
View File
@@ -1 +1,2 @@
bin bin
release
+17 -4
View File
@@ -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,13 +28,17 @@
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
on: on:
tags: true tags: true
branches: branches:
only: only:
# Pushes and PR to the master branch # Pushes and PR to the master branch
+17
View File
@@ -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
View File
@@ -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"
+40 -10
View File
@@ -1,23 +1,53 @@
.PHONY: all deps osxkeychain test validate wincred .PHONY: all deps osxkeychain secretservice test validate wincred
TRAVIS_OS_NAME ?= linux
VERSION = 0.5.0
all: test all: test
deps: deps:
go get github.com/golang/lint/golint go get github.com/golang/lint/golint
clean:
rm -rf bin
rm -rf release
osxkeychain: osxkeychain:
mkdir -p bin mkdir 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
osxrelease: clean test codesign
mkdir -p release
cd bin && tar cvfz ../release/docker-credential-osxkeychain-v$(VERSION)-amd64.tar.gz docker-credential-osxkeychain
secretservice:
mkdir bin
go build -o bin/docker-credential-secretservice secretservice/cmd/main_linux.go
wincred:
mkdir 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`
+23 -6
View File
@@ -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.
+1
View File
@@ -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
View File
@@ -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
+4
View File
@@ -0,0 +1,4 @@
set -ex
sh -e /etc/init.d/xvfb start
sleep 3 # give xvfb some time to start
+87
View File
@@ -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
}
+207
View File
@@ -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)
}
}
+37
View File
@@ -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
}
+60 -20
View File
@@ -10,36 +10,61 @@ import (
"strings" "strings"
) )
type credentialsGetResponse struct { // Credentials holds the information shared between docker and the credentials store.
Username string type Credentials struct {
Password string ServerURL string
Username string
Secret string
}
// Docker credentials should be labeled as such in credentials stores that allow labelling.
// That label allows to filter out non-Docker credentials too at lookup/search in macOS keychain,
// Windows credentials manager and Linux libsecret. Default value is "Docker Credentials"
var CredsLabel = "Docker Credentials"
func SetCredsLabel(label string) {
CredsLabel = label
} }
// 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 +84,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 +101,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 +120,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 +138,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)
}
+34 -15
View File
@@ -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)
}
}
+37
View File
@@ -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
View File
@@ -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")
+1 -1
View File
@@ -6,5 +6,5 @@ import (
) )
func main() { func main() {
credentials.Serve(osxkeychain.New()) credentials.Serve(osxkeychain.Osxkeychain{})
} }
+136 -6
View File
@@ -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,9 @@ 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 *label, char *username, char *secret) {
SecKeychainItemRef item;
OSStatus status = SecKeychainAddInternetPassword( OSStatus status = SecKeychainAddInternetPassword(
NULL, NULL,
strlen(server->host), server->host, strlen(server->host), server->host,
@@ -20,16 +26,32 @@ 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 &item
); );
if (status) { if (status) {
return get_error(status); return get_error(status);
} }
SecKeychainAttribute attribute;
SecKeychainAttributeList attrs;
attribute.tag = kSecLabelItemAttr;
attribute.data = label;
attribute.length = strlen(label);
attrs.count = 1;
attrs.attr = &attribute;
status = SecKeychainItemModifyContent(item, &attrs, 0, NULL);
if (status) {
return get_error(status);
}
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 +64,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 +118,111 @@ 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 *credsLabel, char *** paths, char *** accts, unsigned int *list_l) {
CFStringRef credsLabelCF = CFStringCreateWithCString(NULL, credsLabel, kCFStringEncodingUTF8);
CFMutableDictionaryRef query = CFDictionaryCreateMutable (NULL, 1, NULL, NULL);
CFDictionaryAddValue(query, kSecClass, kSecClassInternetPassword);
CFDictionaryAddValue(query, kSecReturnAttributes, kCFBooleanTrue);
CFDictionaryAddValue(query, kSecMatchLimit, kSecMatchLimitAll);
CFDictionaryAddValue(query, kSecAttrLabel, credsLabelCF);
//Use this query dictionary
CFTypeRef result= NULL;
OSStatus status = SecItemCopyMatching(
query,
&result);
CFRelease(credsLabelCF);
//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);
}
+56 -22
View File
@@ -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,27 +22,27 @@ 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
} }
defer freeServer(s) defer freeServer(s)
label := C.CString(credentials.CredsLabel)
defer C.free(unsafe.Pointer(label))
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, label, 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 +52,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 +68,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 +78,62 @@ 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) {
credsLabelC := C.CString(credentials.CredsLabel)
defer C.free(unsafe.Pointer(credsLabelC))
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(credsLabelC, &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 {
+4 -2
View File
@@ -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 *label, 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 *credsLabel, char *** data, char *** accts, unsigned int *list_l);
void freeListData(char *** data, unsigned int length);
+29 -11
View File
@@ -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)
} }
} }
+10
View File
@@ -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{})
}
+162
View File
@@ -0,0 +1,162 @@
#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,
{
{ "label", SECRET_SCHEMA_ATTRIBUTE_STRING },
{ "server", SECRET_SCHEMA_ATTRIBUTE_STRING },
{ "username", SECRET_SCHEMA_ATTRIBUTE_STRING },
{ "docker_cli", SECRET_SCHEMA_ATTRIBUTE_STRING },
{ "NULL", 0 },
}
};
return &docker_schema;
}
GError *add(char *label, char *server, char *username, char *secret) {
GError *err = NULL;
secret_password_store_sync (DOCKER_SCHEMA, SECRET_COLLECTION_DEFAULT,
server, secret, NULL, &err,
"label", label,
"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_attribute(const char *attribute, 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, attribute, 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, DOCKER_SCHEMA, 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_attribute("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 *ref_label, 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);
// List credentials with the right label only
g_hash_table_insert(attributes, g_strdup("label"), g_strdup(ref_label));
service = secret_service_get_sync(SECRET_SERVICE_NONE, NULL, &err);
if (err != NULL) {
return err;
}
items = secret_service_search_sync(service, NULL, attributes, flags, NULL, &err);
int numKeys = g_list_length(items);
if (err != NULL) {
return err;
}
char **tmp_paths = (char **) calloc(1,(int)sizeof(char *)*numKeys);
char **tmp_accts = (char **) calloc(1,(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_attribute("username",current->data);
if (acctTmp==NULL) {
acctTmp = "account not defined";
}
tmp_paths[listNumber] = (char *) calloc(1, sizeof(char)*(strlen(pathTmp)+1));
tmp_accts[listNumber] = (char *) calloc(1, sizeof(char)*(strlen(acctTmp)+1));
memcpy(tmp_paths[listNumber], pathTmp, sizeof(char)*(strlen(pathTmp)+1));
memcpy(tmp_accts[listNumber], acctTmp, sizeof(char)*(strlen(acctTmp)+1));
listNumber = listNumber + 1;
}
*paths = (char **) realloc(tmp_paths, (int)sizeof(char *)*listNumber);
*accts = (char **) realloc(tmp_accts, (int)sizeof(char *)*listNumber);
*list_l = listNumber;
return NULL;
}
void freeListData(char *** data, unsigned int length) {
int i;
for(i=0; i<length; i++) {
free((*data)[i]);
}
free(*data);
}
+115
View File
@@ -0,0 +1,115 @@
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")
}
credsLabel := C.CString(credentials.CredsLabel)
defer C.free(unsafe.Pointer(credsLabel))
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(credsLabel, 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 for a given credentials label
func (h Secretservice) List() (map[string]string, error) {
credsLabelC := C.CString(credentials.CredsLabel)
defer C.free(unsafe.Pointer(credsLabelC))
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(credsLabelC, &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)
resp := make(map[string]string)
listLen := int(listLenC)
if listLen == 0 {
return resp, nil
}
pathTmp := (*[1 << 30]*C.char)(unsafe.Pointer(pathsC))[:listLen:listLen]
acctTmp := (*[1 << 30]*C.char)(unsafe.Pointer(acctsC))[:listLen:listLen]
for i := 0; i < listLen; i++ {
resp[C.GoString(pathTmp[i])] = C.GoString(acctTmp[i])
}
return resp, nil
}
+13
View File
@@ -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 *label, char *server, char *username, char *secret);
GError *delete(char *server);
GError *get(char *server, char **username, char **secret);
GError *list(char *label, char *** paths, char *** accts, unsigned int *list_l);
void freeListData(char *** data, unsigned int length);
+90
View File
@@ -0,0 +1,90 @@
package secretservice
import (
"strings"
"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{}
// Check how many docker credentials we have when starting the test
old_auths, err := helper.List()
if err != nil {
t.Fatal(err)
}
// If any docker credentials with the tests values we are providing, we
// remove them as they probably come from a previous failed test
for k, v := range old_auths {
if strings.Compare(k, creds.ServerURL) == 0 && strings.Compare(v, creds.Username) == 0 {
if err := helper.Delete(creds.ServerURL); err != nil {
t.Fatal(err)
}
}
}
// Check again how many docker credentials we have when starting the test
old_auths, err = helper.List()
if err != nil {
t.Fatal(err)
}
// Add new credentials
if err := helper.Add(creds); err != nil {
t.Fatal(err)
}
// Verify that it is inside the secret service store
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)
}
// We should have one more credential than before adding
new_auths, err := helper.List()
if err != nil || (len(new_auths)-len(old_auths) != 1) {
t.Fatal(err)
}
old_auths = new_auths
// Deleting the credentials associated to current server url should succeed
if err := helper.Delete(creds.ServerURL); err != nil {
t.Fatal(err)
}
// We should have one less credential than before deleting
new_auths, err = helper.List()
if err != nil || (len(old_auths)-len(new_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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
+1 -1
View File
@@ -6,5 +6,5 @@ import (
) )
func main() { func main() {
credentials.Serve(wincred.New()) credentials.Serve(wincred.Wincred{})
} }
+34 -11
View File
@@ -1,28 +1,28 @@
package wincred package wincred
import ( import (
"bytes"
winc "github.com/danieljoos/wincred" winc "github.com/danieljoos/wincred"
"github.com/docker/docker-credential-helpers/credentials" "github.com/docker/docker-credential-helpers/credentials"
"strings"
) )
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
g.Attributes = []winc.CredentialAttribute{{"label", []byte(credentials.CredsLabel)}}
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 +34,33 @@ 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 for a given credentials label.
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 {
attrs := creds[i].Attributes
for _, attr := range attrs {
if strings.Compare(attr.Keyword, "label") == 0 &&
bytes.Compare(attr.Value, []byte(credentials.CredsLabel)) == 0 {
resp[creds[i].TargetName] = creds[i].UserName
}
}
}
return resp, nil
}
+58 -8
View File
@@ -2,6 +2,7 @@ package wincred
import ( import (
"testing" "testing"
"strings"
"github.com/docker/docker-credential-helpers/credentials" "github.com/docker/docker-credential-helpers/credentials"
) )
@@ -10,15 +11,45 @@ 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 := Wincred{}
// check for and remove remaining credentials from previous fail tests
oldauths, err := helper.List()
if err != nil {
t.Fatal(err)
}
for k, v := range oldauths {
if strings.Compare(k, creds.ServerURL) == 0 && strings.Compare(v, creds.Username) == 0 {
if err := helper.Delete(creds.ServerURL); err != nil {
t.Fatal(err)
}
} else if strings.Compare(k, creds1.ServerURL) == 0 && strings.Compare(v, creds1.Username) == 0 {
if err := helper.Delete(creds1.ServerURL); err != nil {
t.Fatal(err)
}
}
}
// recount for credentials
oldauths, err = helper.List()
if err != nil {
t.Fatal(err)
} }
helper := New()
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 +58,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) - len(oldauths) != 1 {
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 +87,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)
} }
} }