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

Compare commits

...

114 Commits

Author SHA1 Message Date
Ulrich VACHON 8a9f93a99f Bump version to 0.6.2
Signed-off-by: Ulrich VACHON <ulrich.vachon@docker.com>
2019-05-02 12:10:16 +02:00
Mathieu Champlon 063cca0a6d Merge pull request #97 from s864372002/hotfix/typo
Typo fixed.
2019-04-30 00:43:22 -07:00
Vincent Demeester 152d64310b Merge pull request #139 from ekcasey/server-alias-wincred
make docker-credential-wincred work like docker-credential-osxkeychain
2019-04-30 09:10:39 +02:00
Emily Casey 77e30bd9dd Style fixes - incorporates PR feedback
Signed-off-by: Emily Casey <ecasey@pivotal.io>
Signed-off-by: Danny Joyce <djoyce@pivotal.io>
2019-04-29 10:51:04 -04:00
Guillaume Tardif 74636a1592 Merge pull request #143 from pgayvallet/osx-list-no-error-on-missing-key
Fix docker-credential-osxkeychain list behaviour in case of missing entry in keychain
2019-04-29 16:27:28 +02:00
Emily Casey a3c1b5b757 Fix imports
Signed-off-by: Emily Casey <ecasey@pivotal.io>
2019-04-29 10:04:46 -04:00
Emily Casey 6f4b0a7c06 make docker-credential-wincred work like docker-credential-osxkeychain
* fetch credentials for server with matching hostname if scheme, path, or port are not provided
* if the credential request includes specific scheme, path, or port that does not match entry, don't return
* extract url helpers into a package

Signed-off-by: Emily Casey <ecasey@pivotal.io>
Signed-off-by: Danny Joyce <djoyce@pivotal.io>
2019-04-29 09:26:13 -04:00
pgayvallet 1546024a83 returns empty map instead of error if credentials not found in keychain
Signed-off-by: pgayvallet <pierre.gayvallet@gmail.com>
2019-04-26 18:34:58 +02:00
Sebastiaan van Stijn ecb01138bd Merge pull request #142 from ulrich/fix_travis_ci
Fix Travis configuration, update XCode version, Golang version and set to minimum macOS version to 10.11.
2019-04-26 09:27:06 -07:00
Ulrich VACHON df92c83808 Update Go version to 1.12.x, update XCode version to 10.1, update MacOS minimum supported version to 10.11.
Signed-off-by: Ulrich VACHON <ulrich.vachon@docker.com>
2019-04-26 12:00:35 +02:00
Vincent Demeester 123ba1b7cd Merge pull request #124 from eyJhb/master
pass: changed the way for checking if password-store is initalized
2018-09-25 10:51:22 +02:00
eyjhbb@gmail.com 9c18f033f7 pass: changed error message returned, if password-store is not initialized
Signed-off-by: eyjhbb@gmail.com <eyjhbb@gmail.com>
2018-09-24 15:15:24 +02:00
eyjhbb@gmail.com d6c1f136e4 pass: changed the way for checking if password-store is initalized
Signed-off-by: eyjhbb@gmail.com <eyjhbb@gmail.com>
2018-09-22 12:53:56 +02:00
Vincent Demeester 73e5f5dbfe Merge pull request #29 from dekkagaijin/freefix
C.free(unsafe.Pointer(err)) -> C.g_error_free(err)
2018-07-19 09:47:51 +02:00
Nassim Eddequiouaq 5241b46610 Merge pull request #110 from euank/lazy-init
pass: only init on run, and do so lazily
2018-06-27 14:33:29 +02:00
Vincent Demeester 3cba3913ea pass: add IsInitialized helper
This will be useful for the cli where they check initialization:
https://github.com/docker/cli/blob/be8dab26a3ab589b96788fdb95f3d07378e57b9b/cli/config/credentials/default_store_linux.go#L8-L10

Signed-off-by: Euan Kemp <euank@euank.com>
Signed-off-by: Vincent Demeester <vincent@sbr.pm>
2018-06-27 14:28:47 +02:00
Vincent Demeester 8502b53592 Merge pull request #108 from euank/pass-trim‮‮‮‮‮‮‮‮trim-pass
pass: trim pass show output
2018-06-27 14:24:08 +02:00
Euan Kemp 5da09fd251 pass: only init on run, and do so lazily
This also fixes the following issues:

1. Safe for concurrent initialization still (it was before in 'init',
   but the alternative to this PR is not)
2. Uses the same password directory during init as it does during
   runtime (the change to getPassDir in initialization logic.
3. Prints significantly better errors if initialization fails
4. Has slightly cleaner abstractions by hiding the initialization check
   in 'runPass'

The 4th item there does mean there are a few cases where more work is
done before erroring, but that amount of work is trivial and my manual
audit didn't reveal anything that seemed worrying.

Fixes #96, alternative to #106

Signed-off-by: Euan Kemp <euank@euank.com>
2018-06-27 14:23:22 +02:00
Euan Kemp dd27c246bd pass: trim pass show output
As of 8446a40, pass show will include a newline when showing a password.
This causes the pass helper here to reliably fail to initialize since a
password doesn't round-trip.

Before making this change, the pass test would fail if the installed
password-store version was v1.7.1+, and after this change it passes
again.

Fixes #107

Signed-off-by: Euan Kemp <euank@euank.com>
2018-06-27 14:21:52 +02:00
Vincent Demeester 26deb2937d Merge pull request #109 from euank/better-exec
pass: simplify some code
2018-06-27 14:19:27 +02:00
Euan Kemp a13ff50017 pass: simplify some code
The exec.Command code and os.Getenv implementation were both needlessly
verbose. This replaces them with simpler variations.

Signed-off-by: Euan Kemp <euank@euank.com>
2018-06-27 14:15:36 +02:00
Vincent Demeester 1c295f7de8 Merge pull request #115 from n4ss/fix-appveyor
Fix Windows CI
2018-06-27 14:14:13 +02:00
Nassim 'Nass' Eddequiouaq 093af814ee fix go vet complaining on composite literales
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2018-06-27 14:02:28 +02:00
Vincent Demeester d499cf5cb9 Merge pull request #114 from n4ss/fix-travisci-osxkeychain
fix osxkeychain ci
2018-06-27 13:52:26 +02:00
Nassim 'Nass' Eddequiouaq b049338a6b Fix appveyor windows ci
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2018-06-27 13:45:32 +02:00
Nassim 'Nass' Eddequiouaq 91fc39d57a install yarn on osx for travisci
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2018-06-27 13:22:53 +02:00
Nassim 'Nass' Eddequiouaq 317219f3a6 Fix yarn complaint in travisci
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2018-06-27 12:13:18 +02:00
Nassim 'Nass' Eddequiouaq 21f4937ebc fix osxkeychain ci
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2018-06-27 11:58:22 +02:00
Vincent Demeester 19b711cc92 Merge pull request #100 from vdemeester/update-maintainers
Update MAINTAINERS file with current maintainers
2018-02-14 08:13:21 +01:00
Vincent Demeester 1f635a73ad Update MAINTAINERS file with current maintainers
Signed-off-by: Vincent Demeester <vincent@sbr.pm>
2018-02-12 14:39:43 +01:00
Piano Kang 302e4ae938 Typo fixed.
Signed-off-by: Piano Kang <kang@bridgewell.com>
2017-12-20 13:37:43 +08:00
Jean-Laurent de Morlhon d68f9aeca3 Merge pull request #88 from n4ss/add-pass-config-notice
Add instructions for pass helper configuration
2017-08-30 14:17:07 +02:00
Vincent Demeester 05a9d4c50d Merge pull request #89 from tych0/better-initialization-check
pass: better initialization check
2017-08-29 21:46:31 +02:00
Tycho Andersen c2eec534ee pass: better initialization check
See comment for details, but basically, let's actually use pass to check
that it works.

Signed-off-by: Tycho Andersen <tycho@docker.com>
2017-08-29 08:25:08 -07:00
Nassim 'Nass' Eddequiouaq 5be80ca212 Add instructions for pass helper configuration
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2017-08-28 18:11:27 +02:00
Vincent Demeester f00de1b72f Merge pull request #87 from n4ss/bump-0.6.0
Bump 0.6.0
2017-08-28 14:18:12 +02:00
Nassim 'Nass' Eddequiouaq 72f0375e37 Bump 0.6.0
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2017-08-25 19:37:31 +02:00
Nassim Eddequiouaq 3cce61446f Merge pull request #85 from promiseofcake/ljk-fix-makefile
Fix Makefile issues
2017-08-25 18:50:05 +02:00
Lucas Kacher ec0d036273 Fix Makefile issues
* mkdir fails when `bin` exists
* obtaining golint fails if previously installed (w/out the -u flag)

Signed-off-by: Lucas Kacher <lucas@vsco.co>
2017-08-23 11:08:54 -04:00
Vincent Demeester d3d9934897 Merge pull request #84 from tych0/fix-helpers-protocol
return "" instead of an error when pass isn't present
2017-08-22 20:10:46 +02:00
Tycho Andersen f212ea17df fix test
Signed-off-by: Tycho Andersen <tycho@docker.com>
2017-08-17 10:36:48 -06:00
Tycho Andersen 09e536a128 return "" instead of an error when pass isn't present
It turns out the cred helpers protocol is to return "" and not an error
when a credential isn't present, so let's do that.

Signed-off-by: Tycho Andersen <tycho@docker.com>
2017-08-17 09:19:19 -06:00
Vincent Demeester 3c90bd29a4 Merge pull request #83 from tych0/expose-pass-initialized
pass backend: expose pass initialized flag
2017-08-16 11:06:21 +02:00
Tycho Andersen b8fb9690c8 pass backend: expose pass initialized flag
This way clients don't have to run their own checks, they can just use the
library's detection.

Signed-off-by: Tycho Andersen <tycho@docker.com>
2017-08-14 08:56:59 -06:00
Vincent Demeester 7efaffb4c4 Merge pull request #81 from tych0/pass-backend-and-package
Add `pass` backend and debian packaging for both -secretservice and -pass backends
2017-08-14 16:00:25 +02:00
Nassim Eddequiouaq 6338c06ba4 Merge pull request #69 from shhsu/command_env
A new entry point for calling the cred helpers that allow passing environment variables
2017-08-14 11:15:13 +02:00
Tycho Andersen 86c94d3e30 add a deb package for pass/secret service backends
Note that this single source package produces two binary packages: one for
-pass, and one for -secretservice, so that users can install whichever
password backend (and thus deps) that they want.

Signed-off-by: Tycho Andersen <tycho@docker.com>
2017-08-11 08:15:29 -06:00
Tycho Andersen 1ab1037707 add a pass credential helper backend
Signed-off-by: Tycho Andersen <tycho@docker.com>
2017-08-11 08:15:29 -06:00
Peter Hsu c69c0725bb A new entry point for calling the cred helpers that allow passing environment variables
Signed-off-by: Peter Hsu <shhsu@microsoft.com>
2017-06-30 10:45:22 -07:00
Nassim Eddequiouaq a8de4f6e8a Merge pull request #77 from jeanlaurent/win-release
Adding winrelease target
2017-06-15 12:39:03 +02:00
Jean-Laurent de Morlhon 4fbc86d7d0 Adding winrelease target
Signed-off-by: Jean-Laurent de Morlhon <jeanlaurent@morlhon.net>
2017-06-15 12:03:00 +02:00
Vincent Demeester 2d19ebb7f4 Merge pull request #76 from thaJeztah/bump-0.5.2
Bump version 0.5.2
2017-06-15 11:31:05 +02:00
Sebastiaan van Stijn 054c53824f Bump version 0.5.2
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2017-06-15 11:21:41 +02:00
Sebastiaan van Stijn be1808e3ed Merge pull request #75 from thaJeztah/update-changelog
Update changelog
2017-06-15 11:07:09 +02:00
Sebastiaan van Stijn e1d4c012bc Fix storing URLs without scheme (#72)
* Fix storing URLs without scheme

If secrets are stored without specifying a scheme
(https://), the keychain-helper would interpret the
hostname as _path_, causing lookup of secrets to fail.

This patch makes sure that a scheme is added (if missing).

If no scheme is specified, https:// is used as a default.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>

* Have pre go1.8 compiler able to compile

Signed-off-by: Tibor Vass <teabee89@gmail.com>

* Fix URL parsing with port and no scheme

Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>

* Improve parseURL comment

Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2017-06-15 10:58:17 +02:00
Sebastiaan van Stijn ad6ee5d58d Update changelog
The changelog wasn't updated for each release.

This patch updates the changelog, with changes
taken from the releases page

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2017-06-15 00:46:21 +02:00
Nassim Eddequiouaq 94be56b6f4 Merge pull request #74 from thaJeztah/bump-travis-to-go-18
Update TravisCI to use Go 1.8
2017-06-14 22:08:13 +02:00
Sebastiaan van Stijn 37bf8afe8b Update TravisCI to use Go 1.8
To match the version used in docker

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2017-06-14 21:29:13 +02:00
Nassim Eddequiouaq 3d7e1817f2 Merge pull request #73 from n4ss/travisci-golang1.7
Update TravisCI to use Go 1.7
2017-06-14 17:21:44 +02:00
Nassim 'Nass' Eddequiouaq 5651367281 Update TravisCI to use Go 1.7
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2017-06-14 16:18:01 +02:00
Nassim Eddequiouaq d8f57a18c6 Merge pull request #61 from danieljoos/linux32
Workaround for Linux 32-bit build
2017-05-30 20:56:52 +02:00
Sebastiaan van Stijn 4bc0bc2a85 Merge pull request #67 from jeanlaurent/add-version-command
Add version command
2017-05-30 19:13:34 +02:00
Jean-Laurent de Morlhon 167b137eb4 vet/lint/fmt before release
Signed-off-by: Jean-Laurent de Morlhon <jeanlaurent@morlhon.net>
2017-05-30 18:26:18 +02:00
Jean-Laurent de Morlhon 2a67ef1524 make linter happy
Signed-off-by: Jean-Laurent de Morlhon <jeanlaurent@morlhon.net>
2017-05-30 18:26:18 +02:00
Jean-Laurent de Morlhon 6ba4edf6e9 Read version from sources
Signed-off-by: Jean-Laurent de Morlhon <jeanlaurent@morlhon.net>
2017-05-30 18:26:18 +02:00
Jean-Laurent de Morlhon fa4a4d4f71 Bump Version 0.5.1-> 0.5.2-devel
Signed-off-by: Jean-Laurent de Morlhon <jeanlaurent@morlhon.net>
2017-05-30 18:26:18 +02:00
Jean-Laurent de Morlhon 4d60b372aa Add a version command
Signed-off-by: Jean-Laurent de Morlhon <jeanlaurent@morlhon.net>
2017-05-30 18:26:18 +02:00
Vincent Demeester 607bf3c174 Merge pull request #68 from jeanlaurent/appveyor-build
AppVeyor build
2017-05-30 08:05:06 -07:00
Jean-Laurent de Morlhon 74f4f75bc1 AppVeyor build
Signed-off-by: Jean-Laurent de Morlhon <jeanlaurent@morlhon.net>
2017-05-30 11:59:52 +02:00
Jean-Laurent de Morlhon 0e7779e5a1 Merge pull request #66 from n4ss/release-v0.5.1
Update version tag to v0.5.1
2017-05-29 16:44:20 +02:00
Nassim 'Nass' Eddequiouaq fb9549d396 Update version tag
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2017-05-29 16:32:21 +02:00
Nassim Eddequiouaq ed11c58ebf Prevent invalid credentials: no missing server URL or username (#62)
* Prevent invalid credentials: no missing server URL or username

Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>

* Add missing username/serverURL error checks in credstore client

Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>

* Clean up doc on invalid creds errors and client's error checks

Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>

* Add tests for missing ServerURL/Username

Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>

* Clean isValidCredsMessage prototype

Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>

* Add test for missing server URL and more detailed error msg

Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2017-05-29 10:21:50 +02:00
Vincent Demeester 79ab7059b0 Merge pull request #64 from shhsu/redirect_stderr
Caller of cred helpers would now see the stderr returns from cred helpers
2017-05-26 09:25:21 -07:00
shhsu@microsoft.com 18d35e4984 Caller of cred helpers would now see the stderr returns from cred helpers
Signed-off-by: shhsu@microsoft.com <shhsu@microsoft.com>
2017-05-24 15:41:34 -07:00
Daniel Joos cdde659563 Workaround for Linux 32-bit build
This adds a workaround for the `secretservice` sub-package to build under 32bit Linux.
It limits the total number of list entries to (2^29)-1.
A short explanation of how this number was calculated has been added in a comment.

Signed-off-by: Daniel Joos <daniel@joosweb.de>
2017-05-10 00:38:33 +02:00
Vincent Demeester 71779cf7f5 Merge pull request #56 from simonferquel/align-get-with-list-filtering
[Windows] align get with list behavior
2017-03-22 09:53:49 +01:00
Simon Ferquel 51c78cdc14 [Windows] align get with list behavior
Avoiding to get unlabelled credentials

Signed-off-by: Simon Ferquel <simon.ferquel@docker.com>
2017-03-16 18:16:37 +01:00
Vincent Demeester 120e37f15d Merge pull request #54 from vdemeester/update-maintainers
Add n4ss as maintainer 👼
2017-03-14 19:30:38 +01:00
Vincent Demeester 4962f775bf Add n4ss as maintainer 👼
Signed-off-by: Vincent Demeester <vincent@sbr.pm>
2017-03-14 16:04:27 +01:00
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
Jake Sanders 79f93e5e69 C.free(unsafe.Pointer(err)) -> defer C.g_error_free(err)
Signed-off-by: Jake Sanders <jsand@google.com>
2016-09-15 09:14:53 -07:00
39 changed files with 1536 additions and 191 deletions
+1
View File
@@ -1 +1,2 @@
bin bin
release
+5 -3
View File
@@ -3,19 +3,21 @@
sudo: required sudo: required
language: go language: go
dist: trusty dist: trusty
osx_image: xcode10.1
os: os:
- linux - linux
- osx - osx
notifications: notifications:
email: false email: false
go: go:
- 1.6 - 1.12.x
install: make deps
addons: addons:
apt: apt:
packages: packages:
- libsecret-1-dev - libsecret-1-dev
- pass
before_script: before_script:
- make deps
- "export DISPLAY=:99.0" - "export DISPLAY=:99.0"
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sh ci/before_script_linux.sh; fi - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sh ci/before_script_linux.sh; fi
- make validate - make validate
@@ -27,7 +29,7 @@
deploy: deploy:
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: "$GITHUB_TOKEN"
# upload file artifacts using a glob expression. # upload file artifacts using a glob expression.
# It requires both options `file_glob` and `file`: # It requires both options `file_glob` and `file`:
# https://github.com/travis-ci/dpl/blob/master/lib/dpl/provider/releases.rb#L47-L53 # https://github.com/travis-ci/dpl/blob/master/lib/dpl/provider/releases.rb#L47-L53
+35 -1
View File
@@ -4,6 +4,41 @@ 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.6.0 (Go client, Linux)
- New credential helper on Linux using `pass`
- New entry point for passing environment variables when calling a credential helper
- Add a Makefile rule generating a Windows release binary
### Note
`pass` needs to be configured for `docker-credential-pass` to work properly.
It must be initialized with a `gpg2` key ID. Make sure your GPG key exists is in `gpg2` keyring as `pass` uses `gpg2` instead of the regular `gpg`.
## v0.5.2 (Mac OS X, Windows, Linux)
- Add a `version` command to output the version
- Fix storing URLs without scheme, and use `https://` by default
## v0.5.1 (Go client, Mac OS X, Windows, Linux)
- Redirect credential helpers' standard error to the caller's
- Prevent invalid credentials and credentials queries
## v0.5.0 (Mac OS X)
- Add a label for Docker credentials and filter credentials lookup to filter keychain lookups
## v0.4.2 (Mac OS X, Windows)
- Fix osxkeychain list
- macOS binary is now signed on release
- Generate a `.exe` instead
## v0.4.1 (Mac OS X)
- Fixes to support older version of OSX (10.10, 10.11)
## v0.4.0 (Go client, Mac OS X, Windows, Linux) ## v0.4.0 (Go client, Mac OS X, Windows, Linux)
- Full implementation for OSX ready - Full implementation for OSX ready
@@ -11,7 +46,6 @@ The platform released is identified after the tag name.
- Implement client.List, change list API - Implement client.List, change list API
- mac: delete credentials before adding them to avoid already exist error (fixes #37) - mac: delete credentials before adding them to avoid already exist error (fixes #37)
## v0.3.0 (Go client) ## v0.3.0 (Go client)
- Add Go client library to talk with the native programs. - Add Go client library to talk with the native programs.
+6 -30
View File
@@ -11,20 +11,16 @@
[Org] [Org]
[Org."Core maintainers"] [Org."Core maintainers"]
people = [ people = [
"aaronlehmann",
"calavera",
"coolljt0725", "coolljt0725",
"cpuguy83", "cpuguy83",
"crosbymichael", "crosbymichael",
"dnephin", "dnephin",
"dongluochen",
"duglin", "duglin",
"estesp", "estesp",
"icecrime",
"jhowardmsft", "jhowardmsft",
"lk4d4",
"mavenugo", "mavenugo",
"mhbauer", "mhbauer",
"n4ss",
"runcom", "runcom",
"stevvooe", "stevvooe",
"thajeztah", "thajeztah",
@@ -44,16 +40,6 @@
# ADD YOURSELF HERE IN ALPHABETICAL ORDER # 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] [people.coolljt0725]
Name = "Lei Jitang" Name = "Lei Jitang"
Email = "leijitang@huawei.com" Email = "leijitang@huawei.com"
@@ -74,11 +60,6 @@
Email = "dnephin@gmail.com" Email = "dnephin@gmail.com"
GitHub = "dnephin" GitHub = "dnephin"
[people.dongluochen]
Name = "Dongluo Chen"
Email = "dongluo.chen@docker.com"
GitHub = "dongluochen"
[people.duglin] [people.duglin]
Name = "Doug Davis" Name = "Doug Davis"
Email = "dug@us.ibm.com" Email = "dug@us.ibm.com"
@@ -89,21 +70,11 @@
Email = "estesp@linux.vnet.ibm.com" Email = "estesp@linux.vnet.ibm.com"
GitHub = "estesp" GitHub = "estesp"
[people.icecrime]
Name = "Arnaud Porterie"
Email = "arnaud@docker.com"
GitHub = "icecrime"
[people.jhowardmsft] [people.jhowardmsft]
Name = "John Howard" Name = "John Howard"
Email = "jhoward@microsoft.com" Email = "jhoward@microsoft.com"
GitHub = "jhowardmsft" GitHub = "jhowardmsft"
[people.lk4d4]
Name = "Alexander Morozov"
Email = "lk4d4@docker.com"
GitHub = "lk4d4"
[people.mavenugo] [people.mavenugo]
Name = "Madhu Venugopal" Name = "Madhu Venugopal"
Email = "madhu@docker.com" Email = "madhu@docker.com"
@@ -114,6 +85,11 @@
Email = "mbauer@us.ibm.com" Email = "mbauer@us.ibm.com"
GitHub = "mhbauer" GitHub = "mhbauer"
[people.n4ss]
Name = "Nassim Eddequiouaq"
Email = "eddequiouaq.nassim@gmail.com"
GitHub = "n4ss"
[people.runcom] [people.runcom]
Name = "Antonio Murdaca" Name = "Antonio Murdaca"
Email = "runcom@redhat.com" Email = "runcom@redhat.com"
+48 -5
View File
@@ -1,23 +1,47 @@
.PHONY: all deps osxkeychain secretservice test validate wincred .PHONY: all deps osxkeychain secretservice test validate wincred pass deb
TRAVIS_OS_NAME ?= linux TRAVIS_OS_NAME ?= linux
VERSION := $(shell grep 'const Version' credentials/version.go | awk -F'"' '{ print $$2 }')
all: test all: test
deps: deps:
go get github.com/golang/lint/golint go get -u golang.org/x/lint/golint
clean:
rm -rf bin
rm -rf release
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
osxcodesign: 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 vet_osx lint fmt test osxcodesign
mkdir -p release
@echo "\nPackaging version ${VERSION}\n"
cd bin && tar cvfz ../release/docker-credential-osxkeychain-v$(VERSION)-amd64.tar.gz docker-credential-osxkeychain
secretservice: secretservice:
mkdir -p bin mkdir -p bin
go build -o bin/docker-credential-secretservice secretservice/cmd/main_linux.go go build -o bin/docker-credential-secretservice secretservice/cmd/main_linux.go
pass:
mkdir -p bin
go build -o bin/docker-credential-pass pass/cmd/main_linux.go
wincred: wincred:
mkdir -p bin mkdir -p bin
go build -o bin/docker-credential-wincred wincred/cmd/main_windows.go go build -o bin/docker-credential-wincred.exe wincred/cmd/main_windows.go
winrelease: clean vet_win lint fmt test wincred
mkdir -p release
@echo "\nPackaging version ${VERSION}\n"
cd bin && zip ../release/docker-credential-wincred-v$(VERSION)-amd64.zip docker-credential-wincred.exe
test: test:
# tests all packages except vendor # tests all packages except vendor
@@ -26,14 +50,33 @@ test:
vet: vet_$(TRAVIS_OS_NAME) vet: vet_$(TRAVIS_OS_NAME)
go vet ./credentials go vet ./credentials
vet_win:
go vet ./wincred
vet_osx: vet_osx:
go vet ./osxkeychain go vet ./osxkeychain
vet_linux: vet_linux:
go vet ./secretservice go vet ./secretservice
validate: vet lint:
for p in `go list ./... | grep -v /vendor/`; do \ for p in `go list ./... | grep -v /vendor/`; do \
golint $$p ; \ golint $$p ; \
done done
fmt:
gofmt -s -l `ls **/*.go | grep -v vendor` gofmt -s -l `ls **/*.go | grep -v vendor`
validate: vet lint fmt
BUILDIMG:=docker-credential-secretservice-$(VERSION)
deb:
mkdir -p release
docker build -f deb/Dockerfile \
--build-arg VERSION=$(VERSION) \
--build-arg DISTRO=xenial \
--tag $(BUILDIMG) \
.
docker run --rm --net=none $(BUILDIMG) tar cf - /release | tar xf -
docker rmi $(BUILDIMG)
+7 -1
View File
@@ -16,7 +16,7 @@ The programs in this repository are written with the Go programming language. Th
$ go get github.com/docker/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 an executable in the `bin` directory inside the repository.
``` ```
$ cd $GOPATH/docker/docker-credentials-helpers $ cd $GOPATH/docker/docker-credentials-helpers
@@ -55,6 +55,12 @@ You can see examples of each function in the [client](https://godoc.org/github.c
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. secretservice: Provides a helper to use the D-Bus secret service as credentials 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. 3. wincred: Provides a helper to use Windows credentials manager as store.
4. pass: Provides a helper to use `pass` as credentials store.
#### Note
`pass` needs to be configured for `docker-credential-pass` to work properly.
It must be initialized with a `gpg2` key ID. Make sure your GPG key exists is in `gpg2` keyring as `pass` uses `gpg2` instead of the regular `gpg`.
## Development ## Development
+12 -55
View File
@@ -1,67 +1,24 @@
version: "{build}" image: Visual Studio 2015
# Source Config
clone_folder: c:\gopath\src\github.com\docker\docker-credential-helpers
# Build host
environment: environment:
global:
GOPATH: c:\gopath GOPATH: c:\gopath
CGO_ENABLED: 1
GOVERSION: 1.6
matrix:
- platform: x86
GOARCH: 386
MSYS2_BITS: 32
- platform: x64
GOARCH: amd64
MSYS2_BITS: 64
init: stack: go 1.8.7
- git config --global core.autocrlf input
# Build clone_folder: c:\gopath\src\github.com\docker\docker-credential-helpers
clone_depth: 10
install: before_build:
# Install Go 1.6. - set PATH=%PATH%;C:\MinGW\bin;
- rmdir c:\go /s /q
- appveyor DownloadFile https://storage.googleapis.com/golang/go%GOVERSION%.windows-%GOARCH%.msi
- msiexec /i go%GOVERSION%.windows-%GOARCH%.msi /q
- set Path=c:\msys64\mingw%MSYS2_BITS%\bin;c:\go\bin;%Path%
- go version
- go env
build: false build_script:
- mingw32-make vet_win wincred
test_script: test_script:
- go vet ./wincred - mingw32-make test
- go test -v github.com/docker/docker-credential-helpers/wincred
# Equivalent to `before_deploy` phase deploy: off
after_test:
# build binary
- mkdir bin
- go build -o bin/docker-credential-wincred wincred/cmd/main_windows.go
# build zipfile, will look like docker-credential-wincred-v0.1.0-amd64.zip in the root directory
- cd bin && 7z a ../docker-credential-wincred-%APPVEYOR_REPO_TAG_NAME%-%GOARCH%.zip docker-credential-wincred
# IMPORTANT All the artifacts need to be listed here, or they won't be uploaded to GitHub
artifacts: artifacts:
- path: docker-credential-wincred-$(APPVEYOR_REPO_TAG_NAME)-$(GOARCH).zip - path: bin/docker-credential-wincred.exe
name: docker-credential-wincred-$(APPVEYOR_REPO_TAG_NAME)-$(GOARCH).zip
deploy: configuration: Release
# 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/
auth_token:
secure: ixWmTXZs8aV5+9s6vPXziIcdMMLd+lBVINJ0K/Sy++2wllpRxUec4/TPVKUGLqvL
provider: GitHub
# deploy when a new tag is pushed
on:
appveyor_repo_tag: true
branches:
only:
- master
+18
View File
@@ -2,3 +2,21 @@ set -ex
sh -e /etc/init.d/xvfb start sh -e /etc/init.d/xvfb start
sleep 3 # give xvfb some time to start sleep 3 # give xvfb some time to start
# init key for pass
gpg --batch --gen-key <<-EOF
%echo Generating a standard key
Key-Type: DSA
Key-Length: 1024
Subkey-Type: ELG-E
Subkey-Length: 1024
Name-Real: Meshuggah Rocks
Name-Email: meshuggah@example.com
Expire-Date: 0
# Do a commit here, so that we can later print "done" :-)
%commit
%echo done
EOF
key=$(gpg --no-auto-check-trustdb --list-secret-keys | grep ^sec | cut -d/ -f2 | cut -d" " -f1)
pass init $key
+36 -2
View File
@@ -9,12 +9,27 @@ import (
"github.com/docker/docker-credential-helpers/credentials" "github.com/docker/docker-credential-helpers/credentials"
) )
// isValidCredsMessage checks if 'msg' contains invalid credentials error message.
// It returns whether the logs are free of invalid credentials errors and the error if it isn't.
// error values can be errCredentialsMissingServerURL or errCredentialsMissingUsername.
func isValidCredsMessage(msg string) error {
if credentials.IsCredentialsMissingServerURLMessage(msg) {
return credentials.NewErrCredentialsMissingServerURL()
}
if credentials.IsCredentialsMissingUsernameMessage(msg) {
return credentials.NewErrCredentialsMissingUsername()
}
return nil
}
// Store uses an external program to save credentials. // Store uses an external program to save credentials.
func Store(program ProgramFunc, credentials *credentials.Credentials) error { func Store(program ProgramFunc, creds *credentials.Credentials) error {
cmd := program("store") cmd := program("store")
buffer := new(bytes.Buffer) buffer := new(bytes.Buffer)
if err := json.NewEncoder(buffer).Encode(credentials); err != nil { if err := json.NewEncoder(buffer).Encode(creds); err != nil {
return err return err
} }
cmd.Input(buffer) cmd.Input(buffer)
@@ -22,6 +37,11 @@ func Store(program ProgramFunc, credentials *credentials.Credentials) error {
out, err := cmd.Output() out, err := cmd.Output()
if err != nil { if err != nil {
t := strings.TrimSpace(string(out)) t := strings.TrimSpace(string(out))
if isValidErr := isValidCredsMessage(t); isValidErr != nil {
err = isValidErr
}
return fmt.Errorf("error storing credentials - err: %v, out: `%s`", err, t) return fmt.Errorf("error storing credentials - err: %v, out: `%s`", err, t)
} }
@@ -41,6 +61,10 @@ func Get(program ProgramFunc, serverURL string) (*credentials.Credentials, error
return nil, credentials.NewErrCredentialsNotFound() return nil, credentials.NewErrCredentialsNotFound()
} }
if isValidErr := isValidCredsMessage(t); isValidErr != nil {
err = isValidErr
}
return nil, fmt.Errorf("error getting credentials - err: %v, out: `%s`", err, t) return nil, fmt.Errorf("error getting credentials - err: %v, out: `%s`", err, t)
} }
@@ -62,6 +86,11 @@ func Erase(program ProgramFunc, serverURL string) error {
out, err := cmd.Output() out, err := cmd.Output()
if err != nil { if err != nil {
t := strings.TrimSpace(string(out)) t := strings.TrimSpace(string(out))
if isValidErr := isValidCredsMessage(t); isValidErr != nil {
err = isValidErr
}
return fmt.Errorf("error erasing credentials - err: %v, out: `%s`", err, t) return fmt.Errorf("error erasing credentials - err: %v, out: `%s`", err, t)
} }
@@ -75,6 +104,11 @@ func List(program ProgramFunc) (map[string]string, error) {
out, err := cmd.Output() out, err := cmd.Output()
if err != nil { if err != nil {
t := strings.TrimSpace(string(out)) t := strings.TrimSpace(string(out))
if isValidErr := isValidCredsMessage(t); isValidErr != nil {
err = isValidErr
}
return nil, fmt.Errorf("error listing credentials - err: %v, out: `%s`", err, t) return nil, fmt.Errorf("error listing credentials - err: %v, out: `%s`", err, t)
} }
+6
View File
@@ -56,6 +56,8 @@ func (m *mockProgram) Output() ([]byte, error) {
return []byte(credentials.NewErrCredentialsNotFound().Error()), errProgramExited return []byte(credentials.NewErrCredentialsNotFound().Error()), errProgramExited
case invalidServerAddress: case invalidServerAddress:
return []byte("program failed"), errProgramExited return []byte("program failed"), errProgramExited
case "":
return []byte(credentials.NewErrCredentialsMissingServerURL().Error()), errProgramExited
} }
case "store": case "store":
var c credentials.Credentials var c credentials.Credentials
@@ -158,12 +160,16 @@ func TestGet(t *testing.T) {
} }
} }
missingServerURLErr := credentials.NewErrCredentialsMissingServerURL()
invalid := []struct { invalid := []struct {
serverURL string serverURL string
err string err string
}{ }{
{missingCredsAddress, credentials.NewErrCredentialsNotFound().Error()}, {missingCredsAddress, credentials.NewErrCredentialsNotFound().Error()},
{invalidServerAddress, "error getting credentials - err: exited 1, out: `program failed`"}, {invalidServerAddress, "error getting credentials - err: exited 1, out: `program failed`"},
{"", fmt.Sprintf("error getting credentials - err: %s, out: `%s`",
missingServerURLErr.Error(), missingServerURLErr.Error())},
} }
for _, v := range invalid { for _, v := range invalid {
+21 -2
View File
@@ -1,7 +1,9 @@
package client package client
import ( import (
"fmt"
"io" "io"
"os"
"os/exec" "os/exec"
) )
@@ -16,9 +18,26 @@ type ProgramFunc func(args ...string) Program
// NewShellProgramFunc creates programs that are executed in a Shell. // NewShellProgramFunc creates programs that are executed in a Shell.
func NewShellProgramFunc(name string) ProgramFunc { func NewShellProgramFunc(name string) ProgramFunc {
return func(args ...string) Program { return NewShellProgramFuncWithEnv(name, nil)
return &Shell{cmd: exec.Command(name, args...)}
} }
// NewShellProgramFuncWithEnv creates programs that are executed in a Shell with environment variables
func NewShellProgramFuncWithEnv(name string, env *map[string]string) ProgramFunc {
return func(args ...string) Program {
return &Shell{cmd: createProgramCmdRedirectErr(name, args, env)}
}
}
func createProgramCmdRedirectErr(commandName string, args []string, env *map[string]string) *exec.Cmd {
programCmd := exec.Command(commandName, args...)
programCmd.Env = os.Environ()
if env != nil {
for k, v := range *env {
programCmd.Env = append(programCmd.Env, fmt.Sprintf("%s=%s", k, v))
}
}
programCmd.Stderr = os.Stderr
return programCmd
} }
// Shell invokes shell commands to talk with a remote credentials helper. // Shell invokes shell commands to talk with a remote credentials helper.
+46 -1
View File
@@ -17,6 +17,32 @@ type Credentials struct {
Secret string Secret string
} }
// isValid checks the integrity of Credentials object such that no credentials lack
// a server URL or a username.
// It returns whether the credentials are valid and the error if it isn't.
// error values can be errCredentialsMissingServerURL or errCredentialsMissingUsername
func (c *Credentials) isValid() (bool, error) {
if len(c.ServerURL) == 0 {
return false, NewErrCredentialsMissingServerURL()
}
if len(c.Username) == 0 {
return false, NewErrCredentialsMissingUsername()
}
return true, nil
}
// CredsLabel holds the way 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"
// SetCredsLabel is a simple setter for CredsLabel
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. // 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.Args[1] as the key for the action.
@@ -25,7 +51,7 @@ type Credentials struct {
func Serve(helper Helper) { func Serve(helper Helper) {
var err error var err error
if len(os.Args) != 2 { if len(os.Args) != 2 {
err = fmt.Errorf("Usage: %s <store|get|erase|list>", os.Args[0]) err = fmt.Errorf("Usage: %s <store|get|erase|list|version>", os.Args[0])
} }
if err == nil { if err == nil {
@@ -49,6 +75,8 @@ func HandleCommand(helper Helper, key string, in io.Reader, out io.Writer) error
return Erase(helper, in) return Erase(helper, in)
case "list": case "list":
return List(helper, out) return List(helper, out)
case "version":
return PrintVersion(out)
} }
return fmt.Errorf("Unknown credential action `%s`", key) return fmt.Errorf("Unknown credential action `%s`", key)
} }
@@ -72,6 +100,10 @@ func Store(helper Helper, reader io.Reader) error {
return err return err
} }
if ok, err := creds.isValid(); !ok {
return err
}
return helper.Add(&creds) return helper.Add(&creds)
} }
@@ -91,6 +123,9 @@ func Get(helper Helper, reader io.Reader, writer io.Writer) error {
} }
serverURL := strings.TrimSpace(buffer.String()) serverURL := strings.TrimSpace(buffer.String())
if len(serverURL) == 0 {
return NewErrCredentialsMissingServerURL()
}
username, secret, err := helper.Get(serverURL) username, secret, err := helper.Get(serverURL)
if err != nil { if err != nil {
@@ -98,6 +133,7 @@ func Get(helper Helper, reader io.Reader, writer io.Writer) error {
} }
resp := Credentials{ resp := Credentials{
ServerURL: serverURL,
Username: username, Username: username,
Secret: secret, Secret: secret,
} }
@@ -126,6 +162,9 @@ func Erase(helper Helper, reader io.Reader) error {
} }
serverURL := strings.TrimSpace(buffer.String()) serverURL := strings.TrimSpace(buffer.String())
if len(serverURL) == 0 {
return NewErrCredentialsMissingServerURL()
}
return helper.Delete(serverURL) return helper.Delete(serverURL)
} }
@@ -139,3 +178,9 @@ func List(helper Helper, writer io.Writer) error {
} }
return json.NewEncoder(writer).Encode(accts) return json.NewEncoder(writer).Encode(accts)
} }
//PrintVersion outputs the current version.
func PrintVersion(writer io.Writer) error {
fmt.Fprintln(writer, Version)
return nil
}
+90
View File
@@ -73,6 +73,46 @@ func TestStore(t *testing.T) {
} }
} }
func TestStoreMissingServerURL(t *testing.T) {
creds := &Credentials{
ServerURL: "",
Username: "foo",
Secret: "bar",
}
b, err := json.Marshal(creds)
if err != nil {
t.Fatal(err)
}
in := bytes.NewReader(b)
h := newMemoryStore()
if err := Store(h, in); IsCredentialsMissingServerURL(err) == false {
t.Fatal(err)
}
}
func TestStoreMissingUsername(t *testing.T) {
creds := &Credentials{
ServerURL: "https://index.docker.io/v1/",
Username: "",
Secret: "bar",
}
b, err := json.Marshal(creds)
if err != nil {
t.Fatal(err)
}
in := bytes.NewReader(b)
h := newMemoryStore()
if err := Store(h, in); IsCredentialsMissingUsername(err) == false {
t.Fatal(err)
}
}
func TestGet(t *testing.T) { func TestGet(t *testing.T) {
serverURL := "https://index.docker.io/v1/" serverURL := "https://index.docker.io/v1/"
creds := &Credentials{ creds := &Credentials{
@@ -115,6 +155,32 @@ func TestGet(t *testing.T) {
} }
} }
func TestGetMissingServerURL(t *testing.T) {
serverURL := "https://index.docker.io/v1/"
creds := &Credentials{
ServerURL: serverURL,
Username: "foo",
Secret: "bar",
}
b, err := json.Marshal(creds)
if err != nil {
t.Fatal(err)
}
in := bytes.NewReader(b)
h := newMemoryStore()
if err := Store(h, in); err != nil {
t.Fatal(err)
}
buf := strings.NewReader("")
w := new(bytes.Buffer)
if err := Get(h, buf, w); IsCredentialsMissingServerURL(err) == false {
t.Fatal(err)
}
}
func TestErase(t *testing.T) { func TestErase(t *testing.T) {
serverURL := "https://index.docker.io/v1/" serverURL := "https://index.docker.io/v1/"
creds := &Credentials{ creds := &Credentials{
@@ -144,6 +210,30 @@ func TestErase(t *testing.T) {
} }
} }
func TestEraseMissingServerURL(t *testing.T) {
serverURL := "https://index.docker.io/v1/"
creds := &Credentials{
ServerURL: serverURL,
Username: "foo",
Secret: "bar",
}
b, err := json.Marshal(creds)
if err != nil {
t.Fatal(err)
}
in := bytes.NewReader(b)
h := newMemoryStore()
if err := Store(h, in); err != nil {
t.Fatal(err)
}
buf := strings.NewReader("")
if err := Erase(h, buf); IsCredentialsMissingServerURL(err) == false {
t.Fatal(err)
}
}
func TestList(t *testing.T) { func TestList(t *testing.T) {
//This tests that there is proper input an output into the byte stream //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 //Individual stores are very OS specific and have been tested in osxkeychain and secretservice respectively
+67 -2
View File
@@ -1,8 +1,15 @@
package credentials package credentials
// ErrCredentialsNotFound standarizes the not found error, so every helper returns const (
// ErrCredentialsNotFound standardizes the not found error, so every helper returns
// the same message and docker can handle it properly. // the same message and docker can handle it properly.
const errCredentialsNotFoundMessage = "credentials not found in native keychain" errCredentialsNotFoundMessage = "credentials not found in native keychain"
// ErrCredentialsMissingServerURL and ErrCredentialsMissingUsername standardize
// invalid credentials or credentials management operations
errCredentialsMissingServerURLMessage = "no credentials server URL"
errCredentialsMissingUsernameMessage = "no credentials username"
)
// errCredentialsNotFound represents an error // errCredentialsNotFound represents an error
// raised when credentials are not in the store. // raised when credentials are not in the store.
@@ -35,3 +42,61 @@ func IsErrCredentialsNotFound(err error) bool {
func IsErrCredentialsNotFoundMessage(err string) bool { func IsErrCredentialsNotFoundMessage(err string) bool {
return err == errCredentialsNotFoundMessage return err == errCredentialsNotFoundMessage
} }
// errCredentialsMissingServerURL represents an error raised
// when the credentials object has no server URL or when no
// server URL is provided to a credentials operation requiring
// one.
type errCredentialsMissingServerURL struct{}
func (errCredentialsMissingServerURL) Error() string {
return errCredentialsMissingServerURLMessage
}
// errCredentialsMissingUsername represents an error raised
// when the credentials object has no username or when no
// username is provided to a credentials operation requiring
// one.
type errCredentialsMissingUsername struct{}
func (errCredentialsMissingUsername) Error() string {
return errCredentialsMissingUsernameMessage
}
// NewErrCredentialsMissingServerURL creates a new error for
// errCredentialsMissingServerURL.
func NewErrCredentialsMissingServerURL() error {
return errCredentialsMissingServerURL{}
}
// NewErrCredentialsMissingUsername creates a new error for
// errCredentialsMissingUsername.
func NewErrCredentialsMissingUsername() error {
return errCredentialsMissingUsername{}
}
// IsCredentialsMissingServerURL returns true if the error
// was an errCredentialsMissingServerURL.
func IsCredentialsMissingServerURL(err error) bool {
_, ok := err.(errCredentialsMissingServerURL)
return ok
}
// IsCredentialsMissingServerURLMessage checks for an
// errCredentialsMissingServerURL in the error message.
func IsCredentialsMissingServerURLMessage(err string) bool {
return err == errCredentialsMissingServerURLMessage
}
// IsCredentialsMissingUsername returns true if the error
// was an errCredentialsMissingUsername.
func IsCredentialsMissingUsername(err error) bool {
_, ok := err.(errCredentialsMissingUsername)
return ok
}
// IsCredentialsMissingUsernameMessage checks for an
// errCredentialsMissingUsername in the error message.
func IsCredentialsMissingUsernameMessage(err string) bool {
return err == errCredentialsMissingUsernameMessage
}
+4
View File
@@ -0,0 +1,4 @@
package credentials
// Version holds a string describing the current version
const Version = "0.6.2"
+19
View File
@@ -0,0 +1,19 @@
FROM ubuntu:xenial
ARG VERSION
ARG DISTRO
RUN apt-get update && apt-get install -yy debhelper dh-make golang-go libsecret-1-dev
RUN mkdir -p /build
WORKDIR /build
ENV GOPATH /build
COPY Makefile .
COPY credentials credentials
COPY secretservice secretservice
COPY pass pass
COPY deb/debian ./debian
COPY deb/build-deb .
RUN /build/build-deb ${VERSION} ${DISTRO}
Executable
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -x
set -e
version=$1
distro=$2
maintainer=$(awk -F ': ' '$1 == "Maintainer" { print $2; exit }' debian/control)
cat > "debian/changelog" <<-EOF
docker-credential-helpers ($version) $DISTRO; urgency=low
* New upstream version
-- $maintainer $(date --rfc-2822)
EOF
mkdir -p src/github.com/docker/docker-credential-helpers
ln -s /build/credentials /build/src/github.com/docker/docker-credential-helpers/credentials
ln -s /build/secretservice /build/src/github.com/docker/docker-credential-helpers/secretservice
ln -s /build/pass /build/src/github.com/docker/docker-credential-helpers/pass
dpkg-buildpackage -us -uc
mkdir /release
mv /docker-credential-* /release
+1
View File
@@ -0,0 +1 @@
9
+25
View File
@@ -0,0 +1,25 @@
Source: docker-credential-helpers
Section: admin
Priority: optional
Maintainer: Docker <support@docker.com>
Homepage: https://dockerproject.org
Standards-Version: 3.9.6
Vcs-Browser: https://github.com/docker/docker-credential-helpers
Vcs-Git: git://github.com/docker/docker-credential-helpers.git
Build-Depends: debhelper
, dh-make
, libsecret-1-dev
Package: docker-credential-secretservice
Architecture: any
Depends: libsecret-1-0
, ${misc:Depends}
Description: docker-credential-secretservice is a credential helper backend
which uses libsecret to keep Docker credentials safe.
Package: docker-credential-pass
Architecture: any
Depends: pass
, ${misc:Depends}
Description: docker-credential-secretservice is a credential helper backend
which uses the pass utility to keep Docker credentials safe.
@@ -0,0 +1 @@
debian/tmp/usr/bin/docker-credential-pass
@@ -0,0 +1 @@
debian/tmp/usr/bin/docker-credential-secretservice
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/make -f
DESTDIR := $(CURDIR)/debian/tmp
override_dh_auto_build:
make secretservice pass
override_dh_auto_install:
install -D bin/docker-credential-secretservice $(DESTDIR)/usr/bin/docker-credential-secretservice
install -D bin/docker-credential-pass $(DESTDIR)/usr/bin/docker-credential-pass
%:
dh $@
override_dh_auto_test:
# no tests
+76 -23
View File
@@ -1,5 +1,6 @@
#include "osxkeychain_darwin.h" #include "osxkeychain_darwin.h"
#include <CoreFoundation/CoreFoundation.h> #include <CoreFoundation/CoreFoundation.h>
#include <Foundation/NSValue.h>
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
@@ -13,7 +14,9 @@ char *get_error(OSStatus status) {
return buf; return buf;
} }
char *keychain_add(struct Server *server, char *username, char *secret) { 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,
@@ -24,11 +27,27 @@ char *keychain_add(struct Server *server, char *username, char *secret) {
server->proto, server->proto,
kSecAuthenticationTypeDefault, kSecAuthenticationTypeDefault,
strlen(secret), secret, 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;
} }
@@ -115,44 +134,42 @@ char * CFStringToCharArr(CFStringRef aString) {
return NULL; return NULL;
} }
char *keychain_list(char *** paths, char *** accts, unsigned int *list_l) { 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); CFMutableDictionaryRef query = CFDictionaryCreateMutable (NULL, 1, NULL, NULL);
CFDictionaryAddValue(query, kSecClass, kSecClassInternetPassword); CFDictionaryAddValue(query, kSecClass, kSecClassInternetPassword);
CFDictionaryAddValue(query, kSecReturnAttributes, kCFBooleanTrue); CFDictionaryAddValue(query, kSecReturnAttributes, kCFBooleanTrue);
CFDictionaryAddValue(query, kSecMatchLimit, kSecMatchLimitAll); CFDictionaryAddValue(query, kSecMatchLimit, kSecMatchLimitAll);
CFDictionaryAddValue(query, kSecAttrLabel, credsLabelCF);
//Use this query dictionary //Use this query dictionary
CFTypeRef result= NULL; CFTypeRef result= NULL;
OSStatus status = SecItemCopyMatching( OSStatus status = SecItemCopyMatching(
query, query,
&result); &result);
CFRelease(credsLabelCF);
//Ran a search and store the results in result //Ran a search and store the results in result
if (status) { if (status) {
return get_error(status); return get_error(status);
} }
int numKeys = CFArrayGetCount(result); CFIndex numKeys = CFArrayGetCount(result);
*paths = (char **) malloc((int)sizeof(char *)*numKeys); *paths = (char **) malloc((int)sizeof(char *)*numKeys);
*accts = (char **) malloc((int)sizeof(char *)*numKeys); *accts = (char **) malloc((int)sizeof(char *)*numKeys);
//result is of type CFArray //result is of type CFArray
for(int i=0; i<numKeys; i++) { for(CFIndex i=0; i<numKeys; i++) {
CFDictionaryRef currKey = CFArrayGetValueAtIndex(result,i); CFDictionaryRef currKey = CFArrayGetValueAtIndex(result,i);
if (CFDictionaryContainsKey(currKey, CFSTR("path"))) {
//Even if a key is stored without an account, Apple defaults it to null so these arrays will be of the same length CFStringRef protocolTmp = CFDictionaryGetValue(currKey, CFSTR("ptcl"));
CFStringRef pathTmp = CFDictionaryGetValue(currKey, CFSTR("path")); if (protocolTmp != NULL) {
CFStringRef acctTmp = CFDictionaryGetValue(currKey, CFSTR("acct")); CFStringRef protocolStr = CFStringCreateWithFormat(NULL, NULL, CFSTR("%@"), protocolTmp);
if (acctTmp == NULL) { if (CFStringCompare(protocolStr, CFSTR("htps"), 0) == kCFCompareEqualTo) {
acctTmp = CFSTR("account not defined"); protocolTmp = CFSTR("https://");
} }
char * path = (char *) malloc(CFStringGetLength(pathTmp)+1); else {
path = CFStringToCharArr(pathTmp); protocolTmp = CFSTR("http://");
path[strlen(path)] = '\0'; }
char * acct = (char *) malloc(CFStringGetLength(acctTmp)+1); CFRelease(protocolStr);
acct = CFStringToCharArr(acctTmp);
acct[strlen(acct)] = '\0';
//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));
} }
else { else {
char * path = "0"; char * path = "0";
@@ -161,9 +178,45 @@ char *keychain_list(char *** paths, char *** accts, unsigned int *list_l) {
memcpy((*paths)[i], path, sizeof(char)*(strlen(path))); memcpy((*paths)[i], path, sizeof(char)*(strlen(path)));
(*accts)[i] = (char *) malloc(sizeof(char)*(strlen(acct))); (*accts)[i] = (char *) malloc(sizeof(char)*(strlen(acct)));
memcpy((*accts)[i], acct, 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);
} }
*list_l = numKeys;
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; return NULL;
} }
+25 -20
View File
@@ -1,8 +1,8 @@
package osxkeychain package osxkeychain
/* /*
#cgo CFLAGS: -x objective-c #cgo CFLAGS: -x objective-c -mmacosx-version-min=10.11
#cgo LDFLAGS: -framework Security -framework Foundation #cgo LDFLAGS: -framework Security -framework Foundation -mmacosx-version-min=10.11
#include "osxkeychain_darwin.h" #include "osxkeychain_darwin.h"
#include <stdlib.h> #include <stdlib.h>
@@ -10,12 +10,11 @@ package osxkeychain
import "C" import "C"
import ( import (
"errors" "errors"
"net/url"
"strconv" "strconv"
"strings"
"unsafe" "unsafe"
"github.com/docker/docker-credential-helpers/credentials" "github.com/docker/docker-credential-helpers/credentials"
"github.com/docker/docker-credential-helpers/registryurl"
) )
// errCredentialsNotFound is the specific error message returned by OS X // errCredentialsNotFound is the specific error message returned by OS X
@@ -35,12 +34,14 @@ func (h Osxkeychain) Add(creds *credentials.Credentials) error {
} }
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))
secret := C.CString(creds.Secret) secret := C.CString(creds.Secret)
defer C.free(unsafe.Pointer(secret)) defer C.free(unsafe.Pointer(secret))
errMsg := C.keychain_add(s, username, secret) 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))
@@ -99,15 +100,22 @@ func (h Osxkeychain) Get(serverURL string) (string, string, error) {
// List returns the stored URLs and corresponding usernames. // List returns the stored URLs and corresponding usernames.
func (h Osxkeychain) List() (map[string]string, error) { func (h Osxkeychain) List() (map[string]string, error) {
credsLabelC := C.CString(credentials.CredsLabel)
defer C.free(unsafe.Pointer(credsLabelC))
var pathsC **C.char var pathsC **C.char
defer C.free(unsafe.Pointer(pathsC)) defer C.free(unsafe.Pointer(pathsC))
var acctsC **C.char var acctsC **C.char
defer C.free(unsafe.Pointer(acctsC)) defer C.free(unsafe.Pointer(acctsC))
var listLenC C.uint var listLenC C.uint
errMsg := C.keychain_list(&pathsC, &acctsC, &listLenC) errMsg := C.keychain_list(credsLabelC, &pathsC, &acctsC, &listLenC)
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 {
return make(map[string]string), nil
}
return nil, errors.New(goMsg) return nil, errors.New(goMsg)
} }
@@ -130,30 +138,27 @@ func (h Osxkeychain) List() (map[string]string, error) {
} }
func splitServer(serverURL string) (*C.struct_Server, error) { func splitServer(serverURL string) (*C.struct_Server, error) {
u, err := url.Parse(serverURL) u, err := registryurl.Parse(serverURL)
if err != nil { if err != nil {
return nil, err return nil, err
} }
hostAndPort := strings.Split(u.Host, ":")
host := hostAndPort[0]
var port int
if len(hostAndPort) == 2 {
p, err := strconv.Atoi(hostAndPort[1])
if err != nil {
return nil, err
}
port = p
}
proto := C.kSecProtocolTypeHTTPS proto := C.kSecProtocolTypeHTTPS
if u.Scheme != "https" { if u.Scheme == "http" {
proto = C.kSecProtocolTypeHTTP proto = C.kSecProtocolTypeHTTP
} }
var port int
p := registryurl.GetPort(u)
if p != "" {
port, err = strconv.Atoi(p)
if err != nil {
return nil, err
}
}
return &C.struct_Server{ return &C.struct_Server{
proto: C.SecProtocolType(proto), proto: C.SecProtocolType(proto),
host: C.CString(host), host: C.CString(registryurl.GetHostname(u)),
port: C.uint(port), port: C.uint(port),
path: C.CString(u.Path), path: C.CString(u.Path),
}, nil }, nil
+2 -2
View File
@@ -7,8 +7,8 @@ struct Server {
unsigned int port; unsigned int port;
}; };
char *keychain_add(struct Server *server, char *username, char *secret); 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 *secret_l, char **secret); 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); char *keychain_list(char *credsLabel, char *** data, char *** accts, unsigned int *list_l);
void freeListData(char *** data, unsigned int length); void freeListData(char *** data, unsigned int length);
+151 -1
View File
@@ -1,8 +1,10 @@
package osxkeychain package osxkeychain
import ( import (
"github.com/docker/docker-credential-helpers/credentials" "fmt"
"testing" "testing"
"github.com/docker/docker-credential-helpers/credentials"
) )
func TestOSXKeychainHelper(t *testing.T) { func TestOSXKeychainHelper(t *testing.T) {
@@ -54,6 +56,154 @@ func TestOSXKeychainHelper(t *testing.T) {
} }
} }
// TestOSXKeychainHelperRetrieveAliases verifies that secrets can be accessed
// through variations on the URL
func TestOSXKeychainHelperRetrieveAliases(t *testing.T) {
tests := []struct {
storeURL string
readURL string
}{
// stored with port, retrieved without
{"https://foobar.docker.io:2376", "https://foobar.docker.io"},
// stored as https, retrieved without scheme
{"https://foobar.docker.io:2376", "foobar.docker.io"},
// stored with path, retrieved without
{"https://foobar.docker.io:1234/one/two", "https://foobar.docker.io:1234"},
}
helper := Osxkeychain{}
defer func() {
for _, te := range tests {
helper.Delete(te.storeURL)
}
}()
// Clean store before testing.
for _, te := range tests {
helper.Delete(te.storeURL)
}
for _, te := range tests {
c := &credentials.Credentials{ServerURL: te.storeURL, Username: "hello", Secret: "world"}
if err := helper.Add(c); err != nil {
t.Errorf("Error: failed to store secret for URL %q: %s", te.storeURL, err)
continue
}
if _, _, err := helper.Get(te.readURL); err != nil {
t.Errorf("Error: failed to read secret for URL %q using %q", te.storeURL, te.readURL)
}
helper.Delete(te.storeURL)
}
}
// TestOSXKeychainHelperRetrieveStrict verifies that only matching secrets are
// returned.
func TestOSXKeychainHelperRetrieveStrict(t *testing.T) {
tests := []struct {
storeURL string
readURL string
}{
// stored as https, retrieved using http
{"https://foobar.docker.io:2376", "http://foobar.docker.io:2376"},
// stored as http, retrieved using https
{"http://foobar.docker.io:2376", "https://foobar.docker.io:2376"},
// same: stored as http, retrieved without a scheme specified (hence, using the default https://)
{"http://foobar.docker.io", "foobar.docker.io:5678"},
// non-matching ports
{"https://foobar.docker.io:1234", "https://foobar.docker.io:5678"},
// non-matching ports TODO is this desired behavior? The other way round does work
//{"https://foobar.docker.io", "https://foobar.docker.io:5678"},
// non-matching paths
{"https://foobar.docker.io:1234/one/two", "https://foobar.docker.io:1234/five/six"},
}
helper := Osxkeychain{}
defer func() {
for _, te := range tests {
helper.Delete(te.storeURL)
}
}()
// Clean store before testing.
for _, te := range tests {
helper.Delete(te.storeURL)
}
for _, te := range tests {
c := &credentials.Credentials{ServerURL: te.storeURL, Username: "hello", Secret: "world"}
if err := helper.Add(c); err != nil {
t.Errorf("Error: failed to store secret for URL %q: %s", te.storeURL, err)
continue
}
if _, _, err := helper.Get(te.readURL); err == nil {
t.Errorf("Error: managed to read secret for URL %q using %q, but should not be able to", te.storeURL, te.readURL)
}
helper.Delete(te.storeURL)
}
}
// TestOSXKeychainHelperStoreRetrieve verifies that secrets stored in the
// the keychain can be read back using the URL that was used to store them.
func TestOSXKeychainHelperStoreRetrieve(t *testing.T) {
tests := []struct {
url string
}{
{url: "foobar.docker.io"},
{url: "foobar.docker.io:2376"},
{url: "//foobar.docker.io:2376"},
{url: "https://foobar.docker.io:2376"},
{url: "http://foobar.docker.io:2376"},
{url: "https://foobar.docker.io:2376/some/path"},
{url: "https://foobar.docker.io:2376/some/other/path"},
{url: "https://foobar.docker.io:2376/some/other/path?foo=bar"},
}
helper := Osxkeychain{}
defer func() {
for _, te := range tests {
helper.Delete(te.url)
}
}()
// Clean store before testing.
for _, te := range tests {
helper.Delete(te.url)
}
// Note that we don't delete between individual tests here, to verify that
// subsequent stores/overwrites don't affect storing / retrieving secrets.
for i, te := range tests {
c := &credentials.Credentials{
ServerURL: te.url,
Username: fmt.Sprintf("user-%d", i),
Secret: fmt.Sprintf("secret-%d", i),
}
if err := helper.Add(c); err != nil {
t.Errorf("Error: failed to store secret for URL: %s: %s", te.url, err)
continue
}
user, secret, err := helper.Get(te.url)
if err != nil {
t.Errorf("Error: failed to read secret for URL %q: %s", te.url, err)
continue
}
if user != c.Username {
t.Errorf("Error: expected username %s, got username %s for URL: %s", c.Username, user, te.url)
}
if secret != c.Secret {
t.Errorf("Error: expected secret %s, got secret %s for URL: %s", c.Secret, secret, te.url)
}
}
}
func TestMissingCredentials(t *testing.T) { func TestMissingCredentials(t *testing.T) {
helper := Osxkeychain{} helper := Osxkeychain{}
_, _, err := helper.Get("https://adsfasdf.wrewerwer.com/asdfsdddd") _, _, err := helper.Get("https://adsfasdf.wrewerwer.com/asdfsdddd")
+10
View File
@@ -0,0 +1,10 @@
package main
import (
"github.com/docker/docker-credential-helpers/credentials"
"github.com/docker/docker-credential-helpers/pass"
)
func main() {
credentials.Serve(pass.Pass{})
}
+192
View File
@@ -0,0 +1,192 @@
// A `pass` based credential helper. Passwords are stored as arguments to pass
// of the form: "$PASS_FOLDER/base64-url(serverURL)/username". We base64-url
// encode the serverURL, because under the hood pass uses files and folders, so
// /s will get translated into additional folders.
package pass
import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"strings"
"sync"
"github.com/docker/docker-credential-helpers/credentials"
)
const PASS_FOLDER = "docker-credential-helpers"
// Pass handles secrets using Linux secret-service as a store.
type Pass struct{}
// Ideally these would be stored as members of Pass, but since all of Pass's
// methods have value receivers, not pointer receivers, and changing that is
// backwards incompatible, we assume that all Pass instances share the same configuration
// initializationMutex is held while initializing so that only one 'pass'
// round-tripping is done to check pass is functioning.
var initializationMutex sync.Mutex
var passInitialized bool
// CheckInitialized checks whether the password helper can be used. It
// internally caches and so may be safely called multiple times with no impact
// on performance, though the first call may take longer.
func (p Pass) CheckInitialized() bool {
return p.checkInitialized() == nil
}
func (p Pass) checkInitialized() error {
initializationMutex.Lock()
defer initializationMutex.Unlock()
if passInitialized {
return nil
}
// We just run a `pass ls`, if it fails then pass is not initialized.
_, err := p.runPassHelper("", "ls")
if err != nil {
return fmt.Errorf("pass not initialized: %v", err)
}
passInitialized = true
return nil
}
func (p Pass) runPass(stdinContent string, args ...string) (string, error) {
if err := p.checkInitialized(); err != nil {
return "", err
}
return p.runPassHelper(stdinContent, args...)
}
func (p Pass) runPassHelper(stdinContent string, args ...string) (string, error) {
var stdout, stderr bytes.Buffer
cmd := exec.Command("pass", args...)
cmd.Stdin = strings.NewReader(stdinContent)
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
return "", fmt.Errorf("%s: %s", err, stderr.String())
}
// trim newlines; pass v1.7.1+ includes a newline at the end of `show` output
return strings.TrimRight(stdout.String(), "\n\r"), nil
}
// Add adds new credentials to the keychain.
func (h Pass) Add(creds *credentials.Credentials) error {
if creds == nil {
return errors.New("missing credentials")
}
encoded := base64.URLEncoding.EncodeToString([]byte(creds.ServerURL))
_, err := h.runPass(creds.Secret, "insert", "-f", "-m", path.Join(PASS_FOLDER, encoded, creds.Username))
return err
}
// Delete removes credentials from the store.
func (h Pass) Delete(serverURL string) error {
if serverURL == "" {
return errors.New("missing server url")
}
encoded := base64.URLEncoding.EncodeToString([]byte(serverURL))
_, err := h.runPass("", "rm", "-rf", path.Join(PASS_FOLDER, encoded))
return err
}
func getPassDir() string {
passDir := "$HOME/.password-store"
if envDir := os.Getenv("PASSWORD_STORE_DIR"); envDir != "" {
passDir = envDir
}
return os.ExpandEnv(passDir)
}
// listPassDir lists all the contents of a directory in the password store.
// Pass uses fancy unicode to emit stuff to stdout, so rather than try
// and parse this, let's just look at the directory structure instead.
func listPassDir(args ...string) ([]os.FileInfo, error) {
passDir := getPassDir()
p := path.Join(append([]string{passDir, PASS_FOLDER}, args...)...)
contents, err := ioutil.ReadDir(p)
if err != nil {
if os.IsNotExist(err) {
return []os.FileInfo{}, nil
}
return nil, err
}
return contents, nil
}
// Get returns the username and secret to use for a given registry server URL.
func (h Pass) Get(serverURL string) (string, string, error) {
if serverURL == "" {
return "", "", errors.New("missing server url")
}
encoded := base64.URLEncoding.EncodeToString([]byte(serverURL))
if _, err := os.Stat(path.Join(getPassDir(), PASS_FOLDER, encoded)); err != nil {
if os.IsNotExist(err) {
return "", "", nil
}
return "", "", err
}
usernames, err := listPassDir(encoded)
if err != nil {
return "", "", err
}
if len(usernames) < 1 {
return "", "", fmt.Errorf("no usernames for %s", serverURL)
}
actual := strings.TrimSuffix(usernames[0].Name(), ".gpg")
secret, err := h.runPass("", "show", path.Join(PASS_FOLDER, encoded, actual))
return actual, secret, err
}
// List returns the stored URLs and corresponding usernames for a given credentials label
func (h Pass) List() (map[string]string, error) {
servers, err := listPassDir()
if err != nil {
return nil, err
}
resp := map[string]string{}
for _, server := range servers {
if !server.IsDir() {
continue
}
serverURL, err := base64.URLEncoding.DecodeString(server.Name())
if err != nil {
return nil, err
}
usernames, err := listPassDir(server.Name())
if err != nil {
return nil, err
}
if len(usernames) < 1 {
return nil, fmt.Errorf("no usernames for %s", serverURL)
}
resp[string(serverURL)] = strings.TrimSuffix(usernames[0].Name(), ".gpg")
}
return resp, nil
}
+75
View File
@@ -0,0 +1,75 @@
package pass
import (
"strings"
"testing"
"github.com/docker/docker-credential-helpers/credentials"
)
func TestPassHelper(t *testing.T) {
helper := Pass{}
creds := &credentials.Credentials{
ServerURL: "https://foobar.docker.io:2376/v1",
Username: "nothing",
Secret: "isthebestmeshuggahalbum",
}
helper.Add(creds)
creds.ServerURL = "https://foobar.docker.io:9999/v2"
helper.Add(creds)
credsList, err := helper.List()
if err != nil {
t.Fatal(err)
}
for server, username := range credsList {
if !(strings.Contains(server, "2376") ||
strings.Contains(server, "9999")) {
t.Fatalf("invalid url: %s", creds.ServerURL)
}
if username != "nothing" {
t.Fatalf("invalid username: %v", username)
}
u, s, err := helper.Get(server)
if err != nil {
t.Fatal(err)
}
if u != username {
t.Fatalf("invalid username %s", u)
}
if s != "isthebestmeshuggahalbum" {
t.Fatalf("invalid secret: %s", s)
}
err = helper.Delete(server)
if err != nil {
t.Fatal(err)
}
username, _, err = helper.Get(server)
if err != nil {
t.Fatal(err)
}
if username != "" {
t.Fatalf("%s shouldn't exist any more", username)
}
}
credsList, err = helper.List()
if err != nil {
t.Fatal(err)
}
if len(credsList) != 0 {
t.Fatal("didn't delete all creds?")
}
}
+37
View File
@@ -0,0 +1,37 @@
package registryurl
import (
"errors"
"net/url"
"strings"
)
// Parse parses and validates a given serverURL to an url.URL, and
// returns an error if validation failed. Querystring parameters are
// omitted in the resulting URL, because they are not used in the helper.
//
// If serverURL does not have a valid scheme, `//` is used as scheme
// before parsing. This prevents the hostname being used as path,
// and the credentials being stored without host.
func Parse(registryURL string) (*url.URL, error) {
// Check if registryURL has a scheme, otherwise add `//` as scheme.
if !strings.Contains(registryURL, "://") && !strings.HasPrefix(registryURL, "//") {
registryURL = "//" + registryURL
}
u, err := url.Parse(registryURL)
if err != nil {
return nil, err
}
if u.Scheme != "" && u.Scheme != "https" && u.Scheme != "http" {
return nil, errors.New("unsupported scheme: " + u.Scheme)
}
if GetHostname(u) == "" {
return nil, errors.New("no hostname in URL")
}
u.RawQuery = ""
return u, nil
}
+46
View File
@@ -0,0 +1,46 @@
package registryurl
import (
"errors"
"testing"
)
// TestHelperParseURL verifies that a // "scheme" is added to URLs,
// and that invalid URLs produce an error.
func TestHelperParseURL(t *testing.T) {
tests := []struct {
url string
expectedURL string
err error
}{
{url: "foobar.docker.io", expectedURL: "//foobar.docker.io"},
{url: "foobar.docker.io:2376", expectedURL: "//foobar.docker.io:2376"},
{url: "//foobar.docker.io:2376", expectedURL: "//foobar.docker.io:2376"},
{url: "http://foobar.docker.io:2376", expectedURL: "http://foobar.docker.io:2376"},
{url: "https://foobar.docker.io:2376", expectedURL: "https://foobar.docker.io:2376"},
{url: "https://foobar.docker.io:2376/some/path", expectedURL: "https://foobar.docker.io:2376/some/path"},
{url: "https://foobar.docker.io:2376/some/other/path?foo=bar", expectedURL: "https://foobar.docker.io:2376/some/other/path"},
{url: "/foobar.docker.io", err: errors.New("no hostname in URL")},
{url: "ftp://foobar.docker.io:2376", err: errors.New("unsupported scheme: ftp")},
}
for _, te := range tests {
u, err := Parse(te.url)
if te.err == nil && err != nil {
t.Errorf("Error: failed to parse URL %q: %s", te.url, err)
continue
}
if te.err != nil && err == nil {
t.Errorf("Error: expected error %q, got none when parsing URL %q", te.err, te.url)
continue
}
if te.err != nil && err.Error() != te.err.Error() {
t.Errorf("Error: expected error %q, got %q when parsing URL %q", te.err, err, te.url)
continue
}
if u != nil && u.String() != te.expectedURL {
t.Errorf("Error: expected URL: %q, but got %q for URL: %q", te.expectedURL, u.String(), te.url)
}
}
}
+15
View File
@@ -0,0 +1,15 @@
//+build go1.8
package registryurl
import (
url "net/url"
)
func GetHostname(u *url.URL) string {
return u.Hostname()
}
func GetPort(u *url.URL) string {
return u.Port()
}
+41
View File
@@ -0,0 +1,41 @@
//+build !go1.8
package registryurl
import (
url "net/url"
"strings"
)
func GetHostname(u *url.URL) string {
return stripPort(u.Host)
}
func GetPort(u *url.URL) string {
return portOnly(u.Host)
}
func stripPort(hostport string) string {
colon := strings.IndexByte(hostport, ':')
if colon == -1 {
return hostport
}
if i := strings.IndexByte(hostport, ']'); i != -1 {
return strings.TrimPrefix(hostport[:i], "[")
}
return hostport[:colon]
}
func portOnly(hostport string) string {
colon := strings.IndexByte(hostport, ':')
if colon == -1 {
return ""
}
if i := strings.Index(hostport, "]:"); i != -1 {
return hostport[i+len("]:"):]
}
if strings.Contains(hostport, "]") {
return ""
}
return hostport[colon+len(":"):]
}
+35 -21
View File
@@ -7,6 +7,7 @@ const SecretSchema *docker_get_schema(void)
static const SecretSchema docker_schema = { static const SecretSchema docker_schema = {
"io.docker.Credentials", SECRET_SCHEMA_NONE, "io.docker.Credentials", SECRET_SCHEMA_NONE,
{ {
{ "label", SECRET_SCHEMA_ATTRIBUTE_STRING },
{ "server", SECRET_SCHEMA_ATTRIBUTE_STRING }, { "server", SECRET_SCHEMA_ATTRIBUTE_STRING },
{ "username", SECRET_SCHEMA_ATTRIBUTE_STRING }, { "username", SECRET_SCHEMA_ATTRIBUTE_STRING },
{ "docker_cli", SECRET_SCHEMA_ATTRIBUTE_STRING }, { "docker_cli", SECRET_SCHEMA_ATTRIBUTE_STRING },
@@ -16,11 +17,12 @@ const SecretSchema *docker_get_schema(void)
return &docker_schema; return &docker_schema;
} }
GError *add(char *server, char *username, char *secret) { GError *add(char *label, char *server, char *username, char *secret) {
GError *err = NULL; GError *err = NULL;
secret_password_store_sync (DOCKER_SCHEMA, SECRET_COLLECTION_DEFAULT, secret_password_store_sync (DOCKER_SCHEMA, SECRET_COLLECTION_DEFAULT,
server, secret, NULL, &err, server, secret, NULL, &err,
"label", label,
"server", server, "server", server,
"username", username, "username", username,
"docker_cli", "1", "docker_cli", "1",
@@ -40,7 +42,7 @@ GError *delete(char *server) {
return NULL; return NULL;
} }
char *get_username(SecretItem *item) { char *get_attribute(const char *attribute, SecretItem *item) {
GHashTable *attributes; GHashTable *attributes;
GHashTableIter iter; GHashTableIter iter;
gchar *value, *key; gchar *value, *key;
@@ -48,7 +50,7 @@ char *get_username(SecretItem *item) {
attributes = secret_item_get_attributes(item); attributes = secret_item_get_attributes(item);
g_hash_table_iter_init(&iter, attributes); g_hash_table_iter_init(&iter, attributes);
while (g_hash_table_iter_next(&iter, (void **)&key, (void **)&value)) { while (g_hash_table_iter_next(&iter, (void **)&key, (void **)&value)) {
if (strncmp(key, "username", strlen(key)) == 0) if (strncmp(key, attribute, strlen(key)) == 0)
return (char *)value; return (char *)value;
} }
g_hash_table_unref(attributes); g_hash_table_unref(attributes);
@@ -71,7 +73,7 @@ GError *get(char *server, char **username, char **secret) {
service = secret_service_get_sync(SECRET_SERVICE_NONE, NULL, &err); service = secret_service_get_sync(SECRET_SERVICE_NONE, NULL, &err);
if (err == NULL) { if (err == NULL) {
items = secret_service_search_sync(service, NULL, attributes, flags, NULL, &err); items = secret_service_search_sync(service, DOCKER_SCHEMA, attributes, flags, NULL, &err);
if (err == NULL) { if (err == NULL) {
for (l = items; l != NULL; l = g_list_next(l)) { for (l = items; l != NULL; l = g_list_next(l)) {
value = secret_item_get_schema_name(l->data); value = secret_item_get_schema_name(l->data);
@@ -85,7 +87,7 @@ GError *get(char *server, char **username, char **secret) {
*secret = strdup(secret_value_get(secretValue, &length)); *secret = strdup(secret_value_get(secretValue, &length));
secret_value_unref(secretValue); secret_value_unref(secretValue);
} }
*username = get_username(l->data); *username = get_attribute("username", l->data);
} }
g_list_free_full(items, g_object_unref); g_list_free_full(items, g_object_unref);
} }
@@ -98,22 +100,30 @@ GError *get(char *server, char **username, char **secret) {
return NULL; return NULL;
} }
GError *list(char *** paths, char *** accts, unsigned int *list_l) { GError *list(char *ref_label, char *** paths, char *** accts, unsigned int *list_l) {
GList *items; GList *items;
GError *err = NULL; GError *err = NULL;
SecretService *service; SecretService *service;
SecretSearchFlags flags = SECRET_SEARCH_LOAD_SECRETS | SECRET_SEARCH_ALL | SECRET_SEARCH_UNLOCK; SecretSearchFlags flags = SECRET_SEARCH_LOAD_SECRETS | SECRET_SEARCH_ALL | SECRET_SEARCH_UNLOCK;
GHashTable *attributes; GHashTable *attributes = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
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); // 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); 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); items = secret_service_search_sync(service, NULL, attributes, flags, NULL, &err);
int numKeys = g_list_length(items); int numKeys = g_list_length(items);
if (err != NULL) { if (err != NULL) {
return err; return err;
} }
*paths = (char **) malloc((int)sizeof(char *)*numKeys);
*accts = (char **) malloc((int)sizeof(char *)*numKeys); 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 // items now contains our keys from the gnome keyring
// we will now put it in our two lists to return it to go // we will now put it in our two lists to return it to go
GList *current; GList *current;
@@ -121,21 +131,25 @@ GError *list(char *** paths, char *** accts, unsigned int *list_l) {
for(current = items; current!=NULL; current = current->next) { for(current = items; current!=NULL; current = current->next) {
char *pathTmp = secret_item_get_label(current->data); char *pathTmp = secret_item_get_label(current->data);
// you cannot have a key without a label in the gnome keyring // you cannot have a key without a label in the gnome keyring
char *acctTmp = get_username(current->data); char *acctTmp = get_attribute("username",current->data);
if (acctTmp==NULL) { if (acctTmp==NULL) {
acctTmp = "account not defined"; acctTmp = "account not defined";
} }
char *path = (char *) malloc(strlen(pathTmp));
char *acct = (char *) malloc(strlen(acctTmp)); tmp_paths[listNumber] = (char *) calloc(1, sizeof(char)*(strlen(pathTmp)+1));
path = pathTmp; tmp_accts[listNumber] = (char *) calloc(1, sizeof(char)*(strlen(acctTmp)+1));
acct = acctTmp;
(*paths)[listNumber] = (char *) malloc(sizeof(char)*(strlen(path))); memcpy(tmp_paths[listNumber], pathTmp, sizeof(char)*(strlen(pathTmp)+1));
memcpy((*paths)[listNumber], path, sizeof(char)*(strlen(path))); memcpy(tmp_accts[listNumber], acctTmp, sizeof(char)*(strlen(acctTmp)+1));
(*accts)[listNumber] = (char *) malloc(sizeof(char)*(strlen(acct)));
memcpy((*accts)[listNumber], acct, sizeof(char)*(strlen(acct)));
listNumber = listNumber + 1; listNumber = listNumber + 1;
} }
*list_l = numKeys;
*paths = (char **) realloc(tmp_paths, (int)sizeof(char *)*listNumber);
*accts = (char **) realloc(tmp_accts, (int)sizeof(char *)*listNumber);
*list_l = listNumber;
return NULL; return NULL;
} }
+19 -7
View File
@@ -22,6 +22,8 @@ func (h Secretservice) Add(creds *credentials.Credentials) error {
if creds == nil { if creds == nil {
return errors.New("missing credentials") return errors.New("missing credentials")
} }
credsLabel := C.CString(credentials.CredsLabel)
defer C.free(unsafe.Pointer(credsLabel))
server := C.CString(creds.ServerURL) server := C.CString(creds.ServerURL)
defer C.free(unsafe.Pointer(server)) defer C.free(unsafe.Pointer(server))
username := C.CString(creds.Username) username := C.CString(creds.Username)
@@ -29,7 +31,7 @@ func (h Secretservice) Add(creds *credentials.Credentials) error {
secret := C.CString(creds.Secret) secret := C.CString(creds.Secret)
defer C.free(unsafe.Pointer(secret)) defer C.free(unsafe.Pointer(secret))
if err := C.add(server, username, secret); err != nil { if err := C.add(credsLabel, server, username, secret); err != nil {
defer C.g_error_free(err) defer C.g_error_free(err)
errMsg := (*C.char)(unsafe.Pointer(err.message)) errMsg := (*C.char)(unsafe.Pointer(err.message))
return errors.New(C.GoString(errMsg)) return errors.New(C.GoString(errMsg))
@@ -79,25 +81,35 @@ func (h Secretservice) Get(serverURL string) (string, string, error) {
return user, pass, nil return user, pass, nil
} }
// List returns the stored URLs and corresponding usernames. // List returns the stored URLs and corresponding usernames for a given credentials label
func (h Secretservice) List() (map[string]string, error) { func (h Secretservice) List() (map[string]string, error) {
credsLabelC := C.CString(credentials.CredsLabel)
defer C.free(unsafe.Pointer(credsLabelC))
var pathsC **C.char var pathsC **C.char
defer C.free(unsafe.Pointer(pathsC)) defer C.free(unsafe.Pointer(pathsC))
var acctsC **C.char var acctsC **C.char
defer C.free(unsafe.Pointer(acctsC)) defer C.free(unsafe.Pointer(acctsC))
var listLenC C.uint var listLenC C.uint
err := C.list(&pathsC, &acctsC, &listLenC) err := C.list(credsLabelC, &pathsC, &acctsC, &listLenC)
if err != nil { if err != nil {
defer C.free(unsafe.Pointer(err)) defer C.g_error_free(err)
return nil, errors.New("Error from list function in secretservice_linux.c likely due to error in secretservice library") 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(&pathsC, listLenC)
defer C.freeListData(&acctsC, 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) resp := make(map[string]string)
listLen := int(listLenC)
if listLen == 0 {
return resp, nil
}
// The maximum capacity of the following two slices is limited to (2^29)-1 to remain compatible
// with 32-bit platforms. The size of a `*C.char` (a pointer) is 4 Byte on a 32-bit system
// and (2^29)*4 == math.MaxInt32 + 1. -- See issue golang/go#13656
pathTmp := (*[(1 << 29) - 1]*C.char)(unsafe.Pointer(pathsC))[:listLen:listLen]
acctTmp := (*[(1 << 29) - 1]*C.char)(unsafe.Pointer(acctsC))[:listLen:listLen]
for i := 0; i < listLen; i++ { for i := 0; i < listLen; i++ {
resp[C.GoString(pathTmp[i])] = C.GoString(acctTmp[i]) resp[C.GoString(pathTmp[i])] = C.GoString(acctTmp[i])
} }
+2 -2
View File
@@ -6,8 +6,8 @@ const SecretSchema *docker_get_schema(void) G_GNUC_CONST;
#define DOCKER_SCHEMA docker_get_schema() #define DOCKER_SCHEMA docker_get_schema()
GError *add(char *server, char *username, char *secret); GError *add(char *label, char *server, char *username, char *secret);
GError *delete(char *server); GError *delete(char *server);
GError *get(char *server, char **username, char **secret); GError *get(char *server, char **username, char **secret);
GError *list(char *** paths, char *** accts, unsigned int *list_l); GError *list(char *label, char *** paths, char *** accts, unsigned int *list_l);
void freeListData(char *** data, unsigned int length); void freeListData(char *** data, unsigned int length);
+39 -6
View File
@@ -1,6 +1,7 @@
package secretservice package secretservice
import ( import (
"strings"
"testing" "testing"
"github.com/docker/docker-credential-helpers/credentials" "github.com/docker/docker-credential-helpers/credentials"
@@ -16,10 +17,36 @@ func TestSecretServiceHelper(t *testing.T) {
} }
helper := Secretservice{} 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 { if err := helper.Add(creds); err != nil {
t.Fatal(err) t.Fatal(err)
} }
// Verify that it is inside the secret service store
username, secret, err := helper.Get(creds.ServerURL) username, secret, err := helper.Get(creds.ServerURL)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -33,15 +60,21 @@ func TestSecretServiceHelper(t *testing.T) {
t.Fatalf("expected %s, got %s\n", "foobarbaz", secret) 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 { if err := helper.Delete(creds.ServerURL); err != nil {
t.Fatal(err) t.Fatal(err)
} }
auths, err := helper.List()
if err != nil || len(auths) == 0 { // We should have one less credential than before deleting
t.Fatal(err) new_auths, err = helper.List()
} if err != nil || (len(old_auths)-len(new_auths) != 1) {
helper.Add(creds)
if newauths, err := helper.List(); (len(newauths) - len(auths)) != 1 {
t.Fatal(err) t.Fatal(err)
} }
} }
+99 -2
View File
@@ -1,8 +1,13 @@
package wincred package wincred
import ( import (
"bytes"
"net/url"
"strings"
winc "github.com/danieljoos/wincred" winc "github.com/danieljoos/wincred"
"github.com/docker/docker-credential-helpers/credentials" "github.com/docker/docker-credential-helpers/credentials"
"github.com/docker/docker-credential-helpers/registryurl"
) )
// Wincred handles secrets using the Windows credential service. // Wincred handles secrets using the Windows credential service.
@@ -10,10 +15,13 @@ type Wincred struct{}
// 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 {
credsLabels := []byte(credentials.CredsLabel)
g := winc.NewGenericCredential(creds.ServerURL) g := winc.NewGenericCredential(creds.ServerURL)
g.UserName = creds.Username g.UserName = creds.Username
g.CredentialBlob = []byte(creds.Secret) g.CredentialBlob = []byte(creds.Secret)
g.Persist = winc.PersistLocalMachine g.Persist = winc.PersistLocalMachine
g.Attributes = []winc.CredentialAttribute{{Keyword: "label", Value: credsLabels}}
return g.Write() return g.Write()
} }
@@ -31,14 +39,94 @@ 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) target, err := getTarget(serverURL)
if err != nil {
return "", "", err
} else if target == "" {
return "", "", credentials.NewErrCredentialsNotFound()
}
g, _ := winc.GetGenericCredential(target)
if g == nil { if g == nil {
return "", "", credentials.NewErrCredentialsNotFound() return "", "", credentials.NewErrCredentialsNotFound()
} }
for _, attr := range g.Attributes {
if strings.Compare(attr.Keyword, "label") == 0 &&
bytes.Compare(attr.Value, []byte(credentials.CredsLabel)) == 0 {
return g.UserName, string(g.CredentialBlob), nil return g.UserName, string(g.CredentialBlob), nil
} }
}
return "", "", credentials.NewErrCredentialsNotFound()
}
// List returns the stored URLs and corresponding usernames. func getTarget(serverURL string) (string, error) {
s, err := registryurl.Parse(serverURL)
if err != nil {
return serverURL, nil
}
creds, err := winc.List()
if err != nil {
return "", err
}
var targets []string
for i := range creds {
attrs := creds[i].Attributes
for _, attr := range attrs {
if attr.Keyword == "label" && bytes.Equal(attr.Value, []byte(credentials.CredsLabel)) {
targets = append(targets, creds[i].TargetName)
}
}
}
if target, found := findMatch(s, targets, exactMatch); found {
return target, nil
}
if target, found := findMatch(s, targets, approximateMatch); found {
return target, nil
}
return "", nil
}
func findMatch(serverUrl *url.URL, targets []string, matches func(url.URL, url.URL) bool) (string, bool) {
for _, target := range targets {
tURL, err := registryurl.Parse(target)
if err != nil {
continue
}
if matches(*serverUrl, *tURL) {
return target, true
}
}
return "", false
}
func exactMatch(serverURL, target url.URL) bool {
return serverURL.String() == target.String()
}
func approximateMatch(serverURL, target url.URL) bool {
//if scheme is missing assume it is the same as target
if serverURL.Scheme == "" {
serverURL.Scheme = target.Scheme
}
//if port is missing assume it is the same as target
if serverURL.Port() == "" && target.Port() != "" {
serverURL.Host = serverURL.Host + ":" + target.Port()
}
//if path is missing assume it is the same as target
if serverURL.Path == "" {
serverURL.Path = target.Path
}
return serverURL.String() == target.String()
}
// List returns the stored URLs and corresponding usernames for a given credentials label.
func (h Wincred) List() (map[string]string, error) { func (h Wincred) List() (map[string]string, error) {
creds, err := winc.List() creds, err := winc.List()
if err != nil { if err != nil {
@@ -47,7 +135,16 @@ func (h Wincred) List() (map[string]string, error) {
resp := make(map[string]string) resp := make(map[string]string)
for i := range creds { 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 resp[creds[i].TargetName] = creds[i].UserName
} }
}
}
return resp, nil return resp, nil
} }
+176 -1
View File
@@ -1,6 +1,8 @@
package wincred package wincred
import ( import (
"fmt"
"strings"
"testing" "testing"
"github.com/docker/docker-credential-helpers/credentials" "github.com/docker/docker-credential-helpers/credentials"
@@ -19,6 +21,31 @@ func TestWinCredHelper(t *testing.T) {
} }
helper := Wincred{} 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)
}
if err := helper.Add(creds); err != nil { if err := helper.Add(creds); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -37,7 +64,7 @@ func TestWinCredHelper(t *testing.T) {
} }
auths, err := helper.List() auths, err := helper.List()
if err != nil || len(auths) == 0 { if err != nil || len(auths)-len(oldauths) != 1 {
t.Fatal(err) t.Fatal(err)
} }
@@ -60,6 +87,154 @@ func TestWinCredHelper(t *testing.T) {
} }
} }
// TestWinCredHelperRetrieveAliases verifies that secrets can be accessed
// through variations on the URL
func TestWinCredHelperRetrieveAliases(t *testing.T) {
tests := []struct {
storeURL string
readURL string
}{
// stored with port, retrieved without
{"https://foobar.docker.io:2376", "https://foobar.docker.io"},
// stored as https, retrieved without scheme
{"https://foobar.docker.io", "foobar.docker.io"},
// stored with path, retrieved without
{"https://foobar.docker.io/one/two", "https://foobar.docker.io"},
}
helper := Wincred{}
defer func() {
for _, te := range tests {
helper.Delete(te.storeURL)
}
}()
// Clean store before testing.
for _, te := range tests {
helper.Delete(te.storeURL)
}
for _, te := range tests {
c := &credentials.Credentials{ServerURL: te.storeURL, Username: "hello", Secret: "world"}
if err := helper.Add(c); err != nil {
t.Errorf("Error: failed to store secret for URL %q: %s", te.storeURL, err)
continue
}
if _, _, err := helper.Get(te.readURL); err != nil {
t.Errorf("Error: failed to read secret for URL %q using %q", te.storeURL, te.readURL)
}
helper.Delete(te.storeURL)
}
}
// TestWinCredHelperRetrieveStrict verifies that only matching secrets are
// returned.
func TestWinCredHelperRetrieveStrict(t *testing.T) {
tests := []struct {
storeURL string
readURL string
}{
// stored as https, retrieved using http
{"https://foobar.docker.io:2376", "http://foobar.docker.io:2376"},
// stored as http, retrieved using https
{"http://foobar.docker.io:2376", "https://foobar.docker.io:2376"},
// same: stored as http, retrieved without a scheme specified (hence, using the default https://)
{"http://foobar.docker.io", "foobar.docker.io:5678"},
// non-matching ports
{"https://foobar.docker.io:1234", "https://foobar.docker.io:5678"},
// non-matching ports TODO is this desired behavior? The other way round does work
//{"https://foobar.docker.io", "https://foobar.docker.io:5678"},
// non-matching paths
{"https://foobar.docker.io:1234/one/two", "https://foobar.docker.io:1234/five/six"},
}
helper := Wincred{}
defer func() {
for _, te := range tests {
helper.Delete(te.storeURL)
}
}()
// Clean store before testing.
for _, te := range tests {
helper.Delete(te.storeURL)
}
for _, te := range tests {
c := &credentials.Credentials{ServerURL: te.storeURL, Username: "hello", Secret: "world"}
if err := helper.Add(c); err != nil {
t.Errorf("Error: failed to store secret for URL %q: %s", te.storeURL, err)
continue
}
if _, _, err := helper.Get(te.readURL); err == nil {
t.Errorf("Error: managed to read secret for URL %q using %q, but should not be able to", te.storeURL, te.readURL)
}
helper.Delete(te.storeURL)
}
}
// TestWinCredHelperStoreRetrieve verifies that secrets stored in the
// the keychain can be read back using the URL that was used to store them.
func TestWinCredHelperStoreRetrieve(t *testing.T) {
tests := []struct {
url string
}{
{url: "foobar.docker.io"},
{url: "foobar.docker.io:2376"},
{url: "//foobar.docker.io:2376"},
{url: "https://foobar.docker.io:2376"},
{url: "http://foobar.docker.io:2376"},
{url: "https://foobar.docker.io:2376/some/path"},
{url: "https://foobar.docker.io:2376/some/other/path"},
{url: "https://foobar.docker.io:2376/some/other/path?foo=bar"},
}
helper := Wincred{}
defer func() {
for _, te := range tests {
helper.Delete(te.url)
}
}()
// Clean store before testing.
for _, te := range tests {
helper.Delete(te.url)
}
// Note that we don't delete between individual tests here, to verify that
// subsequent stores/overwrites don't affect storing / retrieving secrets.
for i, te := range tests {
c := &credentials.Credentials{
ServerURL: te.url,
Username: fmt.Sprintf("user-%d", i),
Secret: fmt.Sprintf("secret-%d", i),
}
if err := helper.Add(c); err != nil {
t.Errorf("Error: failed to store secret for URL: %s: %s", te.url, err)
continue
}
user, secret, err := helper.Get(te.url)
if err != nil {
t.Errorf("Error: failed to read secret for URL %q: %s", te.url, err)
continue
}
if user != c.Username {
t.Errorf("Error: expected username %s, got username %s for URL: %s", c.Username, user, te.url)
}
if secret != c.Secret {
t.Errorf("Error: expected secret %s, got secret %s for URL: %s", c.Secret, secret, te.url)
}
}
}
func TestMissingCredentials(t *testing.T) { func TestMissingCredentials(t *testing.T) {
helper := Wincred{} helper := Wincred{}
_, _, err := helper.Get("https://adsfasdf.wrewerwer.com/asdfsdddd") _, _, err := helper.Get("https://adsfasdf.wrewerwer.com/asdfsdddd")