Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
K
k3s
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Jacklull
k3s
Commits
8e093b9e
Commit
8e093b9e
authored
Jul 14, 2017
by
Cole Mickens
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
azure: acr: support auth to preview ACR w/ MSI+AAD
parent
ebf24c14
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
358 additions
and
14 deletions
+358
-14
BUILD
pkg/credentialprovider/azure/BUILD
+6
-1
azure_acr_helper.go
pkg/credentialprovider/azure/azure_acr_helper.go
+300
-0
azure_credentials.go
pkg/credentialprovider/azure/azure_credentials.go
+52
-13
No files found.
pkg/credentialprovider/azure/BUILD
View file @
8e093b9e
...
@@ -10,14 +10,19 @@ load(
...
@@ -10,14 +10,19 @@ load(
go_library(
go_library(
name = "go_default_library",
name = "go_default_library",
srcs = ["azure_credentials.go"],
srcs = [
"azure_acr_helper.go",
"azure_credentials.go",
],
tags = ["automanaged"],
tags = ["automanaged"],
deps = [
deps = [
"//pkg/cloudprovider/providers/azure:go_default_library",
"//pkg/cloudprovider/providers/azure:go_default_library",
"//pkg/credentialprovider:go_default_library",
"//pkg/credentialprovider:go_default_library",
"//vendor/github.com/Azure/azure-sdk-for-go/arm/containerregistry:go_default_library",
"//vendor/github.com/Azure/azure-sdk-for-go/arm/containerregistry:go_default_library",
"//vendor/github.com/Azure/go-autorest/autorest:go_default_library",
"//vendor/github.com/Azure/go-autorest/autorest:go_default_library",
"//vendor/github.com/Azure/go-autorest/autorest/adal:go_default_library",
"//vendor/github.com/Azure/go-autorest/autorest/azure:go_default_library",
"//vendor/github.com/Azure/go-autorest/autorest/azure:go_default_library",
"//vendor/github.com/dgrijalva/jwt-go:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/github.com/spf13/pflag:go_default_library",
"//vendor/github.com/spf13/pflag:go_default_library",
],
],
...
...
pkg/credentialprovider/azure/azure_acr_helper.go
0 → 100644
View file @
8e093b9e
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/*
Copyright 2017 Microsoft Corporation
MIT License
Copyright (c) Microsoft Corporation. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE
*/
// Source: https://github.com/Azure/acr-docker-credential-helper/blob/a79b541f3ee761f6cc4511863ed41fb038c19464/src/docker-credential-acr/acr_login.go
package
azure
import
(
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"strings"
"unicode"
jwt
"github.com/dgrijalva/jwt-go"
)
type
authDirective
struct
{
service
string
realm
string
}
type
accessTokenPayload
struct
{
TenantID
string
`json:"tid"`
}
type
acrTokenPayload
struct
{
Expiration
int64
`json:"exp"`
TenantID
string
`json:"tenant"`
Credential
string
`json:"credential"`
}
type
acrAuthResponse
struct
{
RefreshToken
string
`json:"refresh_token"`
}
// 5 minutes buffer time to allow timeshift between local machine and AAD
const
timeShiftBuffer
=
300
const
userAgentHeader
=
"User-Agent"
const
userAgent
=
"kubernetes-credentialprovider-acr"
const
dockerTokenLoginUsernameGUID
=
"00000000-0000-0000-0000-000000000000"
var
client
=
&
http
.
Client
{}
func
receiveChallengeFromLoginServer
(
serverAddress
string
)
(
*
authDirective
,
error
)
{
challengeURL
:=
url
.
URL
{
Scheme
:
"https"
,
Host
:
serverAddress
,
Path
:
"v2/"
,
}
var
err
error
var
r
*
http
.
Request
r
,
_
=
http
.
NewRequest
(
"GET"
,
challengeURL
.
String
(),
nil
)
r
.
Header
.
Add
(
userAgentHeader
,
userAgent
)
var
challenge
*
http
.
Response
if
challenge
,
err
=
client
.
Do
(
r
);
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"Error reaching registry endpoint %s, error: %s"
,
challengeURL
.
String
(),
err
)
}
defer
challenge
.
Body
.
Close
()
if
challenge
.
StatusCode
!=
401
{
return
nil
,
fmt
.
Errorf
(
"Registry did not issue a valid AAD challenge, status: %d"
,
challenge
.
StatusCode
)
}
var
authHeader
[]
string
var
ok
bool
if
authHeader
,
ok
=
challenge
.
Header
[
"Www-Authenticate"
];
!
ok
{
return
nil
,
fmt
.
Errorf
(
"Challenge response does not contain header 'Www-Authenticate'"
)
}
if
len
(
authHeader
)
!=
1
{
return
nil
,
fmt
.
Errorf
(
"Registry did not issue a valid AAD challenge, authenticate header [%s]"
,
strings
.
Join
(
authHeader
,
", "
))
}
authSections
:=
strings
.
SplitN
(
authHeader
[
0
],
" "
,
2
)
authType
:=
strings
.
ToLower
(
authSections
[
0
])
var
authParams
*
map
[
string
]
string
if
authParams
,
err
=
parseAssignments
(
authSections
[
1
]);
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"Unable to understand the contents of Www-Authenticate header %s"
,
authSections
[
1
])
}
// verify headers
if
!
strings
.
EqualFold
(
"Bearer"
,
authType
)
{
return
nil
,
fmt
.
Errorf
(
"Www-Authenticate: expected realm: Bearer, actual: %s"
,
authType
)
}
if
len
((
*
authParams
)[
"service"
])
==
0
{
return
nil
,
fmt
.
Errorf
(
"Www-Authenticate: missing header
\"
service
\"
"
)
}
if
len
((
*
authParams
)[
"realm"
])
==
0
{
return
nil
,
fmt
.
Errorf
(
"Www-Authenticate: missing header
\"
realm
\"
"
)
}
return
&
authDirective
{
service
:
(
*
authParams
)[
"service"
],
realm
:
(
*
authParams
)[
"realm"
],
},
nil
}
func
parseAcrToken
(
identityToken
string
)
(
token
*
acrTokenPayload
,
err
error
)
{
tokenSegments
:=
strings
.
Split
(
identityToken
,
"."
)
if
len
(
tokenSegments
)
<
2
{
return
nil
,
fmt
.
Errorf
(
"Invalid existing refresh token length: %d"
,
len
(
tokenSegments
))
}
payloadSegmentEncoded
:=
tokenSegments
[
1
]
var
payloadBytes
[]
byte
if
payloadBytes
,
err
=
jwt
.
DecodeSegment
(
payloadSegmentEncoded
);
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"Error decoding payload segment from refresh token, error: %s"
,
err
)
}
var
payload
acrTokenPayload
if
err
=
json
.
Unmarshal
(
payloadBytes
,
&
payload
);
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"Error unmarshalling acr payload, error: %s"
,
err
)
}
return
&
payload
,
nil
}
func
performTokenExchange
(
serverAddress
string
,
directive
*
authDirective
,
tenant
string
,
accessToken
string
)
(
string
,
error
)
{
var
err
error
data
:=
url
.
Values
{
"service"
:
[]
string
{
directive
.
service
},
"grant_type"
:
[]
string
{
"access_token_refresh_token"
},
"access_token"
:
[]
string
{
accessToken
},
"refresh_token"
:
[]
string
{
accessToken
},
"tenant"
:
[]
string
{
tenant
},
}
var
realmURL
*
url
.
URL
if
realmURL
,
err
=
url
.
Parse
(
directive
.
realm
);
err
!=
nil
{
return
""
,
fmt
.
Errorf
(
"Www-Authenticate: invalid realm %s"
,
directive
.
realm
)
}
authEndpoint
:=
fmt
.
Sprintf
(
"%s://%s/oauth2/exchange"
,
realmURL
.
Scheme
,
realmURL
.
Host
)
datac
:=
data
.
Encode
()
var
r
*
http
.
Request
r
,
_
=
http
.
NewRequest
(
"POST"
,
authEndpoint
,
bytes
.
NewBufferString
(
datac
))
r
.
Header
.
Add
(
userAgentHeader
,
userAgent
)
r
.
Header
.
Add
(
"Content-Type"
,
"application/x-www-form-urlencoded"
)
r
.
Header
.
Add
(
"Content-Length"
,
strconv
.
Itoa
(
len
(
datac
)))
var
exchange
*
http
.
Response
if
exchange
,
err
=
client
.
Do
(
r
);
err
!=
nil
{
return
""
,
fmt
.
Errorf
(
"Www-Authenticate: failed to reach auth url %s"
,
authEndpoint
)
}
defer
exchange
.
Body
.
Close
()
if
exchange
.
StatusCode
!=
200
{
return
""
,
fmt
.
Errorf
(
"Www-Authenticate: auth url %s responded with status code %d"
,
authEndpoint
,
exchange
.
StatusCode
)
}
var
content
[]
byte
if
content
,
err
=
ioutil
.
ReadAll
(
exchange
.
Body
);
err
!=
nil
{
return
""
,
fmt
.
Errorf
(
"Www-Authenticate: error reading response from %s"
,
authEndpoint
)
}
var
authResp
acrAuthResponse
if
err
=
json
.
Unmarshal
(
content
,
&
authResp
);
err
!=
nil
{
return
""
,
fmt
.
Errorf
(
"Www-Authenticate: unable to read response %s"
,
content
)
}
return
authResp
.
RefreshToken
,
nil
}
// Try and parse a string of assignments in the form of:
// key1 = value1, key2 = "value 2", key3 = ""
// Note: this method and handle quotes but does not handle escaping of quotes
func
parseAssignments
(
statements
string
)
(
*
map
[
string
]
string
,
error
)
{
var
cursor
int
result
:=
make
(
map
[
string
]
string
)
var
errorMsg
=
fmt
.
Errorf
(
"malformed header value: %s"
,
statements
)
for
{
// parse key
equalIndex
:=
nextOccurrence
(
statements
,
cursor
,
"="
)
if
equalIndex
==
-
1
{
return
nil
,
errorMsg
}
key
:=
strings
.
TrimSpace
(
statements
[
cursor
:
equalIndex
])
// parse value
cursor
=
nextNoneSpace
(
statements
,
equalIndex
+
1
)
if
cursor
==
-
1
{
return
nil
,
errorMsg
}
// case: value is quoted
if
statements
[
cursor
]
==
'"'
{
cursor
=
cursor
+
1
// like I said, not handling escapes, but this will skip any comma that's
// within the quotes which is somewhat more likely
closeQuoteIndex
:=
nextOccurrence
(
statements
,
cursor
,
"
\"
"
)
if
closeQuoteIndex
==
-
1
{
return
nil
,
errorMsg
}
value
:=
statements
[
cursor
:
closeQuoteIndex
]
result
[
key
]
=
value
commaIndex
:=
nextNoneSpace
(
statements
,
closeQuoteIndex
+
1
)
if
commaIndex
==
-
1
{
// no more comma, done
return
&
result
,
nil
}
else
if
statements
[
commaIndex
]
!=
','
{
// expect comma immediately after close quote
return
nil
,
errorMsg
}
else
{
cursor
=
commaIndex
+
1
}
}
else
{
commaIndex
:=
nextOccurrence
(
statements
,
cursor
,
","
)
endStatements
:=
commaIndex
==
-
1
var
untrimmed
string
if
endStatements
{
untrimmed
=
statements
[
cursor
:
commaIndex
]
}
else
{
untrimmed
=
statements
[
cursor
:
]
}
value
:=
strings
.
TrimSpace
(
untrimmed
)
if
len
(
value
)
==
0
{
// disallow empty value without quote
return
nil
,
errorMsg
}
result
[
key
]
=
value
if
endStatements
{
return
&
result
,
nil
}
cursor
=
commaIndex
+
1
}
}
}
func
nextOccurrence
(
str
string
,
start
int
,
sep
string
)
int
{
if
start
>=
len
(
str
)
{
return
-
1
}
offset
:=
strings
.
Index
(
str
[
start
:
],
sep
)
if
offset
==
-
1
{
return
-
1
}
return
offset
+
start
}
func
nextNoneSpace
(
str
string
,
start
int
)
int
{
if
start
>=
len
(
str
)
{
return
-
1
}
offset
:=
strings
.
IndexFunc
(
str
[
start
:
],
func
(
c
rune
)
bool
{
return
!
unicode
.
IsSpace
(
c
)
})
if
offset
==
-
1
{
return
-
1
}
return
offset
+
start
}
pkg/credentialprovider/azure/azure_credentials.go
View file @
8e093b9e
...
@@ -23,6 +23,7 @@ import (
...
@@ -23,6 +23,7 @@ import (
"github.com/Azure/azure-sdk-for-go/arm/containerregistry"
"github.com/Azure/azure-sdk-for-go/arm/containerregistry"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/adal"
azureapi
"github.com/Azure/go-autorest/autorest/azure"
azureapi
"github.com/Azure/go-autorest/autorest/azure"
"github.com/golang/glog"
"github.com/golang/glog"
"github.com/spf13/pflag"
"github.com/spf13/pflag"
...
@@ -58,10 +59,11 @@ func NewACRProvider(configFile *string) credentialprovider.DockerConfigProvider
...
@@ -58,10 +59,11 @@ func NewACRProvider(configFile *string) credentialprovider.DockerConfigProvider
}
}
type
acrProvider
struct
{
type
acrProvider
struct
{
file
*
string
file
*
string
config
*
azure
.
Config
config
*
azure
.
Config
environment
*
azureapi
.
Environment
environment
*
azureapi
.
Environment
registryClient
RegistriesClient
registryClient
RegistriesClient
servicePrincipalToken
*
adal
.
ServicePrincipalToken
}
}
func
(
a
*
acrProvider
)
loadConfig
(
rdr
io
.
Reader
)
error
{
func
(
a
*
acrProvider
)
loadConfig
(
rdr
io
.
Reader
)
error
{
...
@@ -92,7 +94,7 @@ func (a *acrProvider) Enabled() bool {
...
@@ -92,7 +94,7 @@ func (a *acrProvider) Enabled() bool {
return
false
return
false
}
}
servicePrincipalToken
,
err
:
=
azure
.
GetServicePrincipalToken
(
a
.
config
,
a
.
environment
)
a
.
servicePrincipalToken
,
err
=
azure
.
GetServicePrincipalToken
(
a
.
config
,
a
.
environment
)
if
err
!=
nil
{
if
err
!=
nil
{
glog
.
Errorf
(
"Failed to create service principal token: %v"
,
err
)
glog
.
Errorf
(
"Failed to create service principal token: %v"
,
err
)
return
false
return
false
...
@@ -100,7 +102,7 @@ func (a *acrProvider) Enabled() bool {
...
@@ -100,7 +102,7 @@ func (a *acrProvider) Enabled() bool {
registryClient
:=
containerregistry
.
NewRegistriesClient
(
a
.
config
.
SubscriptionID
)
registryClient
:=
containerregistry
.
NewRegistriesClient
(
a
.
config
.
SubscriptionID
)
registryClient
.
BaseURI
=
a
.
environment
.
ResourceManagerEndpoint
registryClient
.
BaseURI
=
a
.
environment
.
ResourceManagerEndpoint
registryClient
.
Authorizer
=
autorest
.
NewBearerAuthorizer
(
servicePrincipalToken
)
registryClient
.
Authorizer
=
autorest
.
NewBearerAuthorizer
(
a
.
servicePrincipalToken
)
a
.
registryClient
=
registryClient
a
.
registryClient
=
registryClient
return
true
return
true
...
@@ -108,21 +110,32 @@ func (a *acrProvider) Enabled() bool {
...
@@ -108,21 +110,32 @@ func (a *acrProvider) Enabled() bool {
func
(
a
*
acrProvider
)
Provide
()
credentialprovider
.
DockerConfig
{
func
(
a
*
acrProvider
)
Provide
()
credentialprovider
.
DockerConfig
{
cfg
:=
credentialprovider
.
DockerConfig
{}
cfg
:=
credentialprovider
.
DockerConfig
{}
entry
:=
credentialprovider
.
DockerConfigEntry
{
Username
:
a
.
config
.
AADClientID
,
Password
:
a
.
config
.
AADClientSecret
,
Email
:
dummyRegistryEmail
,
}
glog
.
V
(
4
)
.
Infof
(
"listing registries"
)
res
,
err
:=
a
.
registryClient
.
List
()
res
,
err
:=
a
.
registryClient
.
List
()
if
err
!=
nil
{
if
err
!=
nil
{
glog
.
Errorf
(
"Failed to list registries: %v"
,
err
)
glog
.
Errorf
(
"Failed to list registries: %v"
,
err
)
return
cfg
return
cfg
}
}
for
ix
:=
range
*
res
.
Value
{
for
ix
:=
range
*
res
.
Value
{
loginServer
:=
getLoginServer
((
*
res
.
Value
)[
ix
])
loginServer
:=
getLoginServer
((
*
res
.
Value
)[
ix
])
glog
.
V
(
4
)
.
Infof
(
"Adding Azure Container Registry docker credential for %s"
,
loginServer
)
var
cred
*
credentialprovider
.
DockerConfigEntry
cfg
[
loginServer
]
=
entry
if
a
.
config
.
UseManagedIdentityExtension
{
cred
,
err
=
getACRDockerEntryFromARMToken
(
a
,
loginServer
)
if
err
!=
nil
{
continue
}
}
else
{
cred
=
&
credentialprovider
.
DockerConfigEntry
{
Username
:
a
.
config
.
AADClientID
,
Password
:
a
.
config
.
AADClientSecret
,
Email
:
dummyRegistryEmail
,
}
}
cfg
[
loginServer
]
=
*
cred
}
}
return
cfg
return
cfg
}
}
...
@@ -131,6 +144,32 @@ func getLoginServer(registry containerregistry.Registry) string {
...
@@ -131,6 +144,32 @@ func getLoginServer(registry containerregistry.Registry) string {
return
*
(
*
registry
.
RegistryProperties
)
.
LoginServer
return
*
(
*
registry
.
RegistryProperties
)
.
LoginServer
}
}
func
getACRDockerEntryFromARMToken
(
a
*
acrProvider
,
loginServer
string
)
(
*
credentialprovider
.
DockerConfigEntry
,
error
)
{
armAccessToken
:=
a
.
servicePrincipalToken
.
AccessToken
glog
.
V
(
4
)
.
Infof
(
"discovering auth redirects for: %s"
,
loginServer
)
directive
,
err
:=
receiveChallengeFromLoginServer
(
loginServer
)
if
err
!=
nil
{
glog
.
Errorf
(
"failed to receive challenge: %s"
,
err
)
return
nil
,
err
}
glog
.
V
(
4
)
.
Infof
(
"exchanging an acr refresh_token"
)
registryRefreshToken
,
err
:=
performTokenExchange
(
loginServer
,
directive
,
a
.
config
.
TenantID
,
armAccessToken
)
if
err
!=
nil
{
glog
.
Errorf
(
"failed to perform token exchange: %s"
,
err
)
return
nil
,
err
}
glog
.
V
(
4
)
.
Infof
(
"adding ACR docker config entry for: %s"
,
loginServer
)
return
&
credentialprovider
.
DockerConfigEntry
{
Username
:
dockerTokenLoginUsernameGUID
,
Password
:
registryRefreshToken
,
Email
:
dummyRegistryEmail
,
},
nil
}
func
(
a
*
acrProvider
)
LazyProvide
()
*
credentialprovider
.
DockerConfigEntry
{
func
(
a
*
acrProvider
)
LazyProvide
()
*
credentialprovider
.
DockerConfigEntry
{
return
nil
return
nil
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment