forked from mystiq/dex
server, integration, cmd: Protect Admin API
Admin API now requires a 128 byte base64 encoded secret to be passed in Authorization header, closing up a potential security hole for those who expose this service.
This commit is contained in:
parent
48b3b38c8b
commit
55040c55fa
3 changed files with 61 additions and 6 deletions
|
@ -41,6 +41,9 @@ func main() {
|
||||||
|
|
||||||
adminListen := fs.String("admin-listen", "http://127.0.0.1:5557", "scheme, host and port for listening for administrative operation requests ")
|
adminListen := fs.String("admin-listen", "http://127.0.0.1:5557", "scheme, host and port for listening for administrative operation requests ")
|
||||||
|
|
||||||
|
adminAPISecret := pflag.NewBase64(server.AdminAPISecretLength)
|
||||||
|
fs.Var(adminAPISecret, "admin-api-secret", fmt.Sprintf("A base64-encoded %d byte string which is used to protect the Admin API.", server.AdminAPISecretLength))
|
||||||
|
|
||||||
localConnectorID := fs.String("local-connector", "local", "ID of the local connector")
|
localConnectorID := fs.String("local-connector", "local", "ID of the local connector")
|
||||||
logDebug := fs.Bool("log-debug", false, "log debug-level information")
|
logDebug := fs.Bool("log-debug", false, "log debug-level information")
|
||||||
logTimestamps := fs.Bool("log-timestamps", false, "prefix log lines with timestamps")
|
logTimestamps := fs.Bool("log-timestamps", false, "prefix log lines with timestamps")
|
||||||
|
@ -124,7 +127,7 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
krot := key.NewPrivateKeyRotator(kRepo, *keyPeriod)
|
krot := key.NewPrivateKeyRotator(kRepo, *keyPeriod)
|
||||||
s := server.NewAdminServer(adminAPI, krot)
|
s := server.NewAdminServer(adminAPI, krot, adminAPISecret.String())
|
||||||
h := s.HTTPHandler()
|
h := s.HTTPHandler()
|
||||||
httpsrv := &http.Server{
|
httpsrv := &http.Server{
|
||||||
Addr: adminURL.Host,
|
Addr: adminURL.Host,
|
||||||
|
|
|
@ -14,6 +14,10 @@ import (
|
||||||
"github.com/coreos/dex/user"
|
"github.com/coreos/dex/user"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
adminAPITestSecret = "admin_secret"
|
||||||
|
)
|
||||||
|
|
||||||
type adminAPITestFixtures struct {
|
type adminAPITestFixtures struct {
|
||||||
ur user.UserRepo
|
ur user.UserRepo
|
||||||
pwr user.PasswordInfoRepo
|
pwr user.PasswordInfoRepo
|
||||||
|
@ -58,6 +62,15 @@ var (
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type adminAPITransport struct {
|
||||||
|
secret string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *adminAPITransport) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||||
|
r.Header.Set("Authorization", a.secret)
|
||||||
|
return http.DefaultTransport.RoundTrip(r)
|
||||||
|
}
|
||||||
|
|
||||||
func makeAdminAPITestFixtures() *adminAPITestFixtures {
|
func makeAdminAPITestFixtures() *adminAPITestFixtures {
|
||||||
f := &adminAPITestFixtures{}
|
f := &adminAPITestFixtures{}
|
||||||
|
|
||||||
|
@ -65,9 +78,13 @@ func makeAdminAPITestFixtures() *adminAPITestFixtures {
|
||||||
f.ur = ur
|
f.ur = ur
|
||||||
f.pwr = pwr
|
f.pwr = pwr
|
||||||
f.adAPI = admin.NewAdminAPI(um, f.ur, f.pwr, "local")
|
f.adAPI = admin.NewAdminAPI(um, f.ur, f.pwr, "local")
|
||||||
f.adSrv = server.NewAdminServer(f.adAPI, nil)
|
f.adSrv = server.NewAdminServer(f.adAPI, nil, adminAPITestSecret)
|
||||||
f.hSrv = httptest.NewServer(f.adSrv.HTTPHandler())
|
f.hSrv = httptest.NewServer(f.adSrv.HTTPHandler())
|
||||||
f.hc = &http.Client{}
|
f.hc = &http.Client{
|
||||||
|
Transport: &adminAPITransport{
|
||||||
|
secret: adminAPITestSecret,
|
||||||
|
},
|
||||||
|
}
|
||||||
f.adClient, _ = adminschema.NewWithBasePath(f.hc, f.hSrv.URL)
|
f.adClient, _ = adminschema.NewWithBasePath(f.hc, f.hSrv.URL)
|
||||||
|
|
||||||
return f
|
return f
|
||||||
|
@ -129,6 +146,7 @@ func TestCreateAdmin(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
admn *adminschema.Admin
|
admn *adminschema.Admin
|
||||||
errCode int
|
errCode int
|
||||||
|
secret string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
admn: &adminschema.Admin{
|
admn: &adminschema.Admin{
|
||||||
|
@ -137,6 +155,14 @@ func TestCreateAdmin(t *testing.T) {
|
||||||
},
|
},
|
||||||
errCode: -1,
|
errCode: -1,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
admn: &adminschema.Admin{
|
||||||
|
Email: "foo@example.com",
|
||||||
|
Password: "foopass",
|
||||||
|
},
|
||||||
|
errCode: http.StatusUnauthorized,
|
||||||
|
secret: "bad_secret",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
// duplicate Email
|
// duplicate Email
|
||||||
admn: &adminschema.Admin{
|
admn: &adminschema.Admin{
|
||||||
|
@ -156,6 +182,11 @@ func TestCreateAdmin(t *testing.T) {
|
||||||
for i, tt := range tests {
|
for i, tt := range tests {
|
||||||
func() {
|
func() {
|
||||||
f := makeAdminAPITestFixtures()
|
f := makeAdminAPITestFixtures()
|
||||||
|
if tt.secret != "" {
|
||||||
|
f.hc.Transport = &adminAPITransport{
|
||||||
|
secret: tt.secret,
|
||||||
|
}
|
||||||
|
}
|
||||||
defer f.close()
|
defer f.close()
|
||||||
|
|
||||||
admn, err := f.adClient.Admin.Create(tt.admn).Do()
|
admn, err := f.adClient.Admin.Create(tt.admn).Do()
|
||||||
|
|
|
@ -16,6 +16,7 @@ import (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
AdminAPIVersion = "v1"
|
AdminAPIVersion = "v1"
|
||||||
|
AdminAPISecretLength = 128
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -28,9 +29,10 @@ var (
|
||||||
type AdminServer struct {
|
type AdminServer struct {
|
||||||
adminAPI *admin.AdminAPI
|
adminAPI *admin.AdminAPI
|
||||||
checker health.Checker
|
checker health.Checker
|
||||||
|
secret string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAdminServer(adminAPI *admin.AdminAPI, rotator *key.PrivateKeyRotator) *AdminServer {
|
func NewAdminServer(adminAPI *admin.AdminAPI, rotator *key.PrivateKeyRotator, secret string) *AdminServer {
|
||||||
return &AdminServer{
|
return &AdminServer{
|
||||||
adminAPI: adminAPI,
|
adminAPI: adminAPI,
|
||||||
checker: health.Checker{
|
checker: health.Checker{
|
||||||
|
@ -38,6 +40,7 @@ func NewAdminServer(adminAPI *admin.AdminAPI, rotator *key.PrivateKeyRotator) *A
|
||||||
rotator,
|
rotator,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
secret: secret,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,7 +51,25 @@ func (s *AdminServer) HTTPHandler() http.Handler {
|
||||||
r.GET(AdminGetStateEndpoint, s.getState)
|
r.GET(AdminGetStateEndpoint, s.getState)
|
||||||
r.Handler("GET", httpPathHealth, s.checker)
|
r.Handler("GET", httpPathHealth, s.checker)
|
||||||
r.HandlerFunc("GET", httpPathDebugVars, health.ExpvarHandler)
|
r.HandlerFunc("GET", httpPathDebugVars, health.ExpvarHandler)
|
||||||
return r
|
|
||||||
|
return authorizer(r, s.secret, httpPathHealth, httpPathDebugVars)
|
||||||
|
}
|
||||||
|
|
||||||
|
func authorizer(h http.Handler, secret string, public ...string) http.Handler {
|
||||||
|
publicSet := map[string]struct{}{}
|
||||||
|
for _, p := range public {
|
||||||
|
publicSet[p] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
_, isPublicPath := publicSet[r.URL.Path]
|
||||||
|
|
||||||
|
if !isPublicPath && r.Header.Get("Authorization") != secret {
|
||||||
|
writeAPIError(w, http.StatusUnauthorized, newAPIError(errorAccessDenied, ""))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AdminServer) getAdmin(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
func (s *AdminServer) getAdmin(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||||
|
|
Loading…
Reference in a new issue