Add Azure Provider
This commit is contained in:
parent
d5a332c3f2
commit
10f47e325b
19
README.md
19
README.md
@ -29,6 +29,8 @@ You will need to register an OAuth application with a Provider (Google, Github o
|
|||||||
Valid providers are :
|
Valid providers are :
|
||||||
|
|
||||||
* [Google](#google-auth-provider) *default*
|
* [Google](#google-auth-provider) *default*
|
||||||
|
|
||||||
|
* [Azure](#azure-auth-provider)
|
||||||
* [GitHub](#github-auth-provider)
|
* [GitHub](#github-auth-provider)
|
||||||
* [LinkedIn](#linkedin-auth-provider)
|
* [LinkedIn](#linkedin-auth-provider)
|
||||||
* [MyUSA](#myusa-auth-provider)
|
* [MyUSA](#myusa-auth-provider)
|
||||||
@ -76,6 +78,15 @@ and the user will be checked against all the provided groups.
|
|||||||
|
|
||||||
Note: The user is checked against the group members list on initial authentication and every time the token is refreshed ( about once an hour ).
|
Note: The user is checked against the group members list on initial authentication and every time the token is refreshed ( about once an hour ).
|
||||||
|
|
||||||
|
### Azure Auth Provider
|
||||||
|
|
||||||
|
1. [Add an application](https://azure.microsoft.com/en-us/documentation/articles/active-directory-integrating-applications/) to your Azure Active Directory tenant.
|
||||||
|
2. On the App properties page provide the correct Sign-On URL ie `https//internal.yourcompany.com/oauth2/callback`
|
||||||
|
3. If applicable take note of your `TenantID` and provide it via the `--azure-tenant=<YOUR TENANT ID>` commandline option. Default the `common` tenant is used.
|
||||||
|
|
||||||
|
The Azure AD auth provider uses `openid` as it default scope. It uses `https://graph.windows.net` as a default protected resource. It call to `https://graph.windows.net/me` to get the email address of the user that logs in.
|
||||||
|
|
||||||
|
|
||||||
### GitHub Auth Provider
|
### GitHub Auth Provider
|
||||||
|
|
||||||
1. Create a new project: https://github.com/settings/developers
|
1. Create a new project: https://github.com/settings/developers
|
||||||
@ -102,6 +113,12 @@ For LinkedIn, the registration steps are:
|
|||||||
|
|
||||||
The [MyUSA](https://alpha.my.usa.gov) authentication service ([GitHub](https://github.com/18F/myusa))
|
The [MyUSA](https://alpha.my.usa.gov) authentication service ([GitHub](https://github.com/18F/myusa))
|
||||||
|
|
||||||
|
### Microsoft Azure AD Provider
|
||||||
|
|
||||||
|
For adding an application to the Microsoft Azure AD follow [these steps to add an application](https://azure.microsoft.com/en-us/documentation/articles/active-directory-integrating-applications/).
|
||||||
|
|
||||||
|
Take note of your `TenantId` if applicable for your situation. The `TenantId` can be used to override the default `common` authorization server with a tenant specific server.
|
||||||
|
|
||||||
## Email Authentication
|
## Email Authentication
|
||||||
|
|
||||||
To authorize by email domain use `--email-domain=yourcompany.com`. To authorize individual email addresses use `--authenticated-emails-file=/path/to/file` with one email per line. To authorize all email addresse use `--email-domain=*`.
|
To authorize by email domain use `--email-domain=yourcompany.com`. To authorize individual email addresses use `--authenticated-emails-file=/path/to/file` with one email per line. To authorize all email addresse use `--email-domain=*`.
|
||||||
@ -120,6 +137,7 @@ An example [oauth2_proxy.cfg](contrib/oauth2_proxy.cfg.example) config file is i
|
|||||||
Usage of oauth2_proxy:
|
Usage of oauth2_proxy:
|
||||||
-approval-prompt="force": Oauth approval_prompt
|
-approval-prompt="force": Oauth approval_prompt
|
||||||
-authenticated-emails-file="": authenticate against emails via file (one per line)
|
-authenticated-emails-file="": authenticate against emails via file (one per line)
|
||||||
|
-azure-tenant="common": go to a tenant-specific or common (tenant-independent) endpoint.
|
||||||
-basic-auth-password="": the password to set when passing the HTTP Basic Auth header
|
-basic-auth-password="": the password to set when passing the HTTP Basic Auth header
|
||||||
-client-id="": the OAuth Client ID: ie: "123456.apps.googleusercontent.com"
|
-client-id="": the OAuth Client ID: ie: "123456.apps.googleusercontent.com"
|
||||||
-client-secret="": the OAuth Client Secret
|
-client-secret="": the OAuth Client Secret
|
||||||
@ -151,6 +169,7 @@ Usage of oauth2_proxy:
|
|||||||
-proxy-prefix="/oauth2": the url root path that this proxy should be nested under (e.g. /<oauth2>/sign_in)
|
-proxy-prefix="/oauth2": the url root path that this proxy should be nested under (e.g. /<oauth2>/sign_in)
|
||||||
-redeem-url="": Token redemption endpoint
|
-redeem-url="": Token redemption endpoint
|
||||||
-redirect-url="": the OAuth Redirect URL. ie: "https://internalapp.yourcompany.com/oauth2/callback"
|
-redirect-url="": the OAuth Redirect URL. ie: "https://internalapp.yourcompany.com/oauth2/callback"
|
||||||
|
-resource="": the resource that is being protected. ie: "https://graph.windows.net". Currently only used in the Azure provider.
|
||||||
-request-logging=true: Log requests to stdout
|
-request-logging=true: Log requests to stdout
|
||||||
-scope="": Oauth scope specification
|
-scope="": Oauth scope specification
|
||||||
-signature-key="": GAP-Signature request signature key (algorithm:secretkey)
|
-signature-key="": GAP-Signature request signature key (algorithm:secretkey)
|
||||||
|
2
main.go
2
main.go
@ -38,6 +38,7 @@ func main() {
|
|||||||
flagSet.Var(&skipAuthRegex, "skip-auth-regex", "bypass authentication for requests path's that match (may be given multiple times)")
|
flagSet.Var(&skipAuthRegex, "skip-auth-regex", "bypass authentication for requests path's that match (may be given multiple times)")
|
||||||
|
|
||||||
flagSet.Var(&emailDomains, "email-domain", "authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email")
|
flagSet.Var(&emailDomains, "email-domain", "authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email")
|
||||||
|
flagSet.String("azure-tenant", "common", "go to a tenant-specific or common (tenant-independent) endpoint.")
|
||||||
flagSet.String("github-org", "", "restrict logins to members of this organisation")
|
flagSet.String("github-org", "", "restrict logins to members of this organisation")
|
||||||
flagSet.String("github-team", "", "restrict logins to members of this team")
|
flagSet.String("github-team", "", "restrict logins to members of this team")
|
||||||
flagSet.Var(&googleGroups, "google-group", "restrict logins to members of this google group (may be given multiple times).")
|
flagSet.Var(&googleGroups, "google-group", "restrict logins to members of this google group (may be given multiple times).")
|
||||||
@ -65,6 +66,7 @@ func main() {
|
|||||||
flagSet.String("login-url", "", "Authentication endpoint")
|
flagSet.String("login-url", "", "Authentication endpoint")
|
||||||
flagSet.String("redeem-url", "", "Token redemption endpoint")
|
flagSet.String("redeem-url", "", "Token redemption endpoint")
|
||||||
flagSet.String("profile-url", "", "Profile access endpoint")
|
flagSet.String("profile-url", "", "Profile access endpoint")
|
||||||
|
flagSet.String("resource", "", "The resource that is protected (Azure AD only)")
|
||||||
flagSet.String("validate-url", "", "Access token validation endpoint")
|
flagSet.String("validate-url", "", "Access token validation endpoint")
|
||||||
flagSet.String("scope", "", "OAuth scope specification")
|
flagSet.String("scope", "", "OAuth scope specification")
|
||||||
flagSet.String("approval-prompt", "force", "OAuth approval_prompt")
|
flagSet.String("approval-prompt", "force", "OAuth approval_prompt")
|
||||||
|
@ -25,6 +25,7 @@ type Options struct {
|
|||||||
TLSKeyFile string `flag:"tls-key" cfg:"tls_key_file"`
|
TLSKeyFile string `flag:"tls-key" cfg:"tls_key_file"`
|
||||||
|
|
||||||
AuthenticatedEmailsFile string `flag:"authenticated-emails-file" cfg:"authenticated_emails_file"`
|
AuthenticatedEmailsFile string `flag:"authenticated-emails-file" cfg:"authenticated_emails_file"`
|
||||||
|
AzureTenant string `flag:"azure-tenant" cfg:"azure_tenant"`
|
||||||
EmailDomains []string `flag:"email-domain" cfg:"email_domains"`
|
EmailDomains []string `flag:"email-domain" cfg:"email_domains"`
|
||||||
GitHubOrg string `flag:"github-org" cfg:"github_org"`
|
GitHubOrg string `flag:"github-org" cfg:"github_org"`
|
||||||
GitHubTeam string `flag:"github-team" cfg:"github_team"`
|
GitHubTeam string `flag:"github-team" cfg:"github_team"`
|
||||||
@ -56,6 +57,7 @@ type Options struct {
|
|||||||
LoginURL string `flag:"login-url" cfg:"login_url"`
|
LoginURL string `flag:"login-url" cfg:"login_url"`
|
||||||
RedeemURL string `flag:"redeem-url" cfg:"redeem_url"`
|
RedeemURL string `flag:"redeem-url" cfg:"redeem_url"`
|
||||||
ProfileURL string `flag:"profile-url" cfg:"profile_url"`
|
ProfileURL string `flag:"profile-url" cfg:"profile_url"`
|
||||||
|
ProtectedResource string `flag:"resource" cfg:"resource"`
|
||||||
ValidateURL string `flag:"validate-url" cfg:"validate_url"`
|
ValidateURL string `flag:"validate-url" cfg:"validate_url"`
|
||||||
Scope string `flag:"scope" cfg:"scope"`
|
Scope string `flag:"scope" cfg:"scope"`
|
||||||
ApprovalPrompt string `flag:"approval-prompt" cfg:"approval_prompt"`
|
ApprovalPrompt string `flag:"approval-prompt" cfg:"approval_prompt"`
|
||||||
@ -205,9 +207,12 @@ func parseProviderInfo(o *Options, msgs []string) []string {
|
|||||||
p.RedeemURL, msgs = parseURL(o.RedeemURL, "redeem", msgs)
|
p.RedeemURL, msgs = parseURL(o.RedeemURL, "redeem", msgs)
|
||||||
p.ProfileURL, msgs = parseURL(o.ProfileURL, "profile", msgs)
|
p.ProfileURL, msgs = parseURL(o.ProfileURL, "profile", msgs)
|
||||||
p.ValidateURL, msgs = parseURL(o.ValidateURL, "validate", msgs)
|
p.ValidateURL, msgs = parseURL(o.ValidateURL, "validate", msgs)
|
||||||
|
p.ProtectedResource, msgs = parseURL(o.ProtectedResource, "resource", msgs)
|
||||||
|
|
||||||
o.provider = providers.New(o.Provider, p)
|
o.provider = providers.New(o.Provider, p)
|
||||||
switch p := o.provider.(type) {
|
switch p := o.provider.(type) {
|
||||||
|
case *providers.AzureProvider:
|
||||||
|
p.Configure(o.AzureTenant)
|
||||||
case *providers.GitHubProvider:
|
case *providers.GitHubProvider:
|
||||||
p.SetOrgTeam(o.GitHubOrg, o.GitHubTeam)
|
p.SetOrgTeam(o.GitHubOrg, o.GitHubTeam)
|
||||||
case *providers.GoogleProvider:
|
case *providers.GoogleProvider:
|
||||||
|
86
providers/azure.go
Normal file
86
providers/azure.go
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
package providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/bitly/oauth2_proxy/api"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AzureProvider struct {
|
||||||
|
*ProviderData
|
||||||
|
Tenant string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAzureProvider(p *ProviderData) *AzureProvider {
|
||||||
|
p.ProviderName = "Azure"
|
||||||
|
|
||||||
|
if p.ProfileURL == nil || p.ProfileURL.String() == "" {
|
||||||
|
p.ProfileURL = &url.URL{
|
||||||
|
Scheme: "https",
|
||||||
|
Host: "graph.windows.net",
|
||||||
|
Path: "/me",
|
||||||
|
RawQuery: "api-version=1.6",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if p.ProtectedResource == nil || p.ProtectedResource.String() == "" {
|
||||||
|
p.ProtectedResource = &url.URL{
|
||||||
|
Scheme: "https",
|
||||||
|
Host: "graph.windows.net",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if p.Scope == "" {
|
||||||
|
p.Scope = "openid"
|
||||||
|
}
|
||||||
|
|
||||||
|
return &AzureProvider{ProviderData: p}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *AzureProvider) Configure(tenant string) {
|
||||||
|
p.Tenant = tenant
|
||||||
|
if tenant == "" {
|
||||||
|
p.Tenant = "common"
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.LoginURL == nil || p.LoginURL.String() == "" {
|
||||||
|
p.LoginURL = &url.URL{
|
||||||
|
Scheme: "https",
|
||||||
|
Host: "login.microsoftonline.com",
|
||||||
|
Path: "/" + p.Tenant + "/oauth2/authorize"}
|
||||||
|
}
|
||||||
|
if p.RedeemURL == nil || p.RedeemURL.String() == "" {
|
||||||
|
p.RedeemURL = &url.URL{
|
||||||
|
Scheme: "https",
|
||||||
|
Host: "login.microsoftonline.com",
|
||||||
|
Path: "/" + p.Tenant + "/oauth2/token",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAzureHeader(access_token string) http.Header {
|
||||||
|
header := make(http.Header)
|
||||||
|
header.Set("Authorization", fmt.Sprintf("Bearer %s", access_token))
|
||||||
|
return header
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *AzureProvider) GetEmailAddress(s *SessionState) (string, error) {
|
||||||
|
if s.AccessToken == "" {
|
||||||
|
return "", errors.New("missing access token")
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest("GET", p.ProfileURL.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
req.Header = getAzureHeader(s.AccessToken)
|
||||||
|
|
||||||
|
json, err := api.Request(req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed making request %s", err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Get("mail").String()
|
||||||
|
}
|
135
providers/azure_test.go
Normal file
135
providers/azure_test.go
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
package providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/bmizerany/assert"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testAzureProvider(hostname string) *AzureProvider {
|
||||||
|
p := NewAzureProvider(
|
||||||
|
&ProviderData{
|
||||||
|
ProviderName: "",
|
||||||
|
LoginURL: &url.URL{},
|
||||||
|
RedeemURL: &url.URL{},
|
||||||
|
ProfileURL: &url.URL{},
|
||||||
|
ValidateURL: &url.URL{},
|
||||||
|
ProtectedResource: &url.URL{},
|
||||||
|
Scope: ""})
|
||||||
|
if hostname != "" {
|
||||||
|
updateURL(p.Data().LoginURL, hostname)
|
||||||
|
updateURL(p.Data().RedeemURL, hostname)
|
||||||
|
updateURL(p.Data().ProfileURL, hostname)
|
||||||
|
updateURL(p.Data().ValidateURL, hostname)
|
||||||
|
updateURL(p.Data().ProtectedResource, hostname)
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAzureProviderDefaults(t *testing.T) {
|
||||||
|
p := testAzureProvider("")
|
||||||
|
assert.NotEqual(t, nil, p)
|
||||||
|
p.Configure("")
|
||||||
|
assert.Equal(t, "Azure", p.Data().ProviderName)
|
||||||
|
assert.Equal(t, "common", p.Tenant)
|
||||||
|
assert.Equal(t, "https://login.microsoftonline.com/common/oauth2/authorize",
|
||||||
|
p.Data().LoginURL.String())
|
||||||
|
assert.Equal(t, "https://login.microsoftonline.com/common/oauth2/token",
|
||||||
|
p.Data().RedeemURL.String())
|
||||||
|
assert.Equal(t, "https://graph.windows.net/me?api-version=1.6",
|
||||||
|
p.Data().ProfileURL.String())
|
||||||
|
assert.Equal(t, "https://graph.windows.net",
|
||||||
|
p.Data().ProtectedResource.String())
|
||||||
|
assert.Equal(t, "",
|
||||||
|
p.Data().ValidateURL.String())
|
||||||
|
assert.Equal(t, "openid", p.Data().Scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAzureProviderOverrides(t *testing.T) {
|
||||||
|
p := NewAzureProvider(
|
||||||
|
&ProviderData{
|
||||||
|
LoginURL: &url.URL{
|
||||||
|
Scheme: "https",
|
||||||
|
Host: "example.com",
|
||||||
|
Path: "/oauth/auth"},
|
||||||
|
RedeemURL: &url.URL{
|
||||||
|
Scheme: "https",
|
||||||
|
Host: "example.com",
|
||||||
|
Path: "/oauth/token"},
|
||||||
|
ProfileURL: &url.URL{
|
||||||
|
Scheme: "https",
|
||||||
|
Host: "example.com",
|
||||||
|
Path: "/oauth/profile"},
|
||||||
|
ValidateURL: &url.URL{
|
||||||
|
Scheme: "https",
|
||||||
|
Host: "example.com",
|
||||||
|
Path: "/oauth/tokeninfo"},
|
||||||
|
ProtectedResource: &url.URL{
|
||||||
|
Scheme: "https",
|
||||||
|
Host: "example.com"},
|
||||||
|
Scope: "profile"})
|
||||||
|
assert.NotEqual(t, nil, p)
|
||||||
|
assert.Equal(t, "Azure", p.Data().ProviderName)
|
||||||
|
assert.Equal(t, "https://example.com/oauth/auth",
|
||||||
|
p.Data().LoginURL.String())
|
||||||
|
assert.Equal(t, "https://example.com/oauth/token",
|
||||||
|
p.Data().RedeemURL.String())
|
||||||
|
assert.Equal(t, "https://example.com/oauth/profile",
|
||||||
|
p.Data().ProfileURL.String())
|
||||||
|
assert.Equal(t, "https://example.com/oauth/tokeninfo",
|
||||||
|
p.Data().ValidateURL.String())
|
||||||
|
assert.Equal(t, "https://example.com",
|
||||||
|
p.Data().ProtectedResource.String())
|
||||||
|
assert.Equal(t, "profile", p.Data().Scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAzureSetTenant(t *testing.T) {
|
||||||
|
p := testAzureProvider("")
|
||||||
|
p.Configure("example")
|
||||||
|
assert.Equal(t, "Azure", p.Data().ProviderName)
|
||||||
|
assert.Equal(t, "example", p.Tenant)
|
||||||
|
assert.Equal(t, "https://login.microsoftonline.com/example/oauth2/authorize",
|
||||||
|
p.Data().LoginURL.String())
|
||||||
|
assert.Equal(t, "https://login.microsoftonline.com/example/oauth2/token",
|
||||||
|
p.Data().RedeemURL.String())
|
||||||
|
assert.Equal(t, "https://graph.windows.net/me?api-version=1.6",
|
||||||
|
p.Data().ProfileURL.String())
|
||||||
|
assert.Equal(t, "https://graph.windows.net",
|
||||||
|
p.Data().ProtectedResource.String())
|
||||||
|
assert.Equal(t, "",
|
||||||
|
p.Data().ValidateURL.String())
|
||||||
|
assert.Equal(t, "openid", p.Data().Scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAzureBackend(payload string) *httptest.Server {
|
||||||
|
path := "/me"
|
||||||
|
query := "api-version=1.6"
|
||||||
|
|
||||||
|
return httptest.NewServer(http.HandlerFunc(
|
||||||
|
func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
url := r.URL
|
||||||
|
if url.Path != path || url.RawQuery != query {
|
||||||
|
w.WriteHeader(404)
|
||||||
|
} else if r.Header.Get("Authorization") != "Bearer imaginary_access_token" {
|
||||||
|
w.WriteHeader(403)
|
||||||
|
} else {
|
||||||
|
w.WriteHeader(200)
|
||||||
|
w.Write([]byte(payload))
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAzureProviderGetEmailAddress(t *testing.T) {
|
||||||
|
b := testAzureBackend(`{ "mail": "user@windows.net" }`)
|
||||||
|
defer b.Close()
|
||||||
|
|
||||||
|
b_url, _ := url.Parse(b.URL)
|
||||||
|
p := testAzureProvider(b_url.Host)
|
||||||
|
|
||||||
|
session := &SessionState{AccessToken: "imaginary_access_token"}
|
||||||
|
email, err := p.GetEmailAddress(session)
|
||||||
|
assert.Equal(t, nil, err)
|
||||||
|
assert.Equal(t, "user@windows.net", email)
|
||||||
|
}
|
@ -11,6 +11,7 @@ type ProviderData struct {
|
|||||||
LoginURL *url.URL
|
LoginURL *url.URL
|
||||||
RedeemURL *url.URL
|
RedeemURL *url.URL
|
||||||
ProfileURL *url.URL
|
ProfileURL *url.URL
|
||||||
|
ProtectedResource *url.URL
|
||||||
ValidateURL *url.URL
|
ValidateURL *url.URL
|
||||||
Scope string
|
Scope string
|
||||||
ApprovalPrompt string
|
ApprovalPrompt string
|
||||||
|
@ -25,6 +25,10 @@ func (p *ProviderData) Redeem(redirectURL, code string) (s *SessionState, err er
|
|||||||
params.Add("client_secret", p.ClientSecret)
|
params.Add("client_secret", p.ClientSecret)
|
||||||
params.Add("code", code)
|
params.Add("code", code)
|
||||||
params.Add("grant_type", "authorization_code")
|
params.Add("grant_type", "authorization_code")
|
||||||
|
if p.ProtectedResource != nil && p.ProtectedResource.String() != "" {
|
||||||
|
params.Add("resource", p.ProtectedResource.String())
|
||||||
|
}
|
||||||
|
|
||||||
var req *http.Request
|
var req *http.Request
|
||||||
req, err = http.NewRequest("POST", p.RedeemURL.String(), bytes.NewBufferString(params.Encode()))
|
req, err = http.NewRequest("POST", p.RedeemURL.String(), bytes.NewBufferString(params.Encode()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -24,6 +24,8 @@ func New(provider string, p *ProviderData) Provider {
|
|||||||
return NewLinkedInProvider(p)
|
return NewLinkedInProvider(p)
|
||||||
case "github":
|
case "github":
|
||||||
return NewGitHubProvider(p)
|
return NewGitHubProvider(p)
|
||||||
|
case "azure":
|
||||||
|
return NewAzureProvider(p)
|
||||||
default:
|
default:
|
||||||
return NewGoogleProvider(p)
|
return NewGoogleProvider(p)
|
||||||
}
|
}
|
||||||
|
@ -41,9 +41,8 @@ func WatchForUpdates(filename string, done <-chan bool, action func()) {
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case _ = <-done:
|
case _ = <-done:
|
||||||
log.Printf("Shutting down watcher for: %s",
|
log.Printf("Shutting down watcher for: %s", filename)
|
||||||
filename)
|
break
|
||||||
return
|
|
||||||
case event := <-watcher.Events:
|
case event := <-watcher.Events:
|
||||||
// On Arch Linux, it appears Chmod events precede Remove events,
|
// On Arch Linux, it appears Chmod events precede Remove events,
|
||||||
// which causes a race between action() and the coming Remove event.
|
// which causes a race between action() and the coming Remove event.
|
||||||
|
Loading…
Reference in New Issue
Block a user