*: add client registration endpoint to admin API

This commit is contained in:
Eric Chiang 2016-04-05 11:37:26 -07:00
parent 0445da2dfe
commit b10645f58d
7 changed files with 119 additions and 21 deletions

View file

@ -4,6 +4,11 @@ package admin
import ( import (
"net/http" "net/http"
"github.com/coreos/go-oidc/oidc"
"github.com/go-gorp/gorp"
"github.com/coreos/dex/client"
"github.com/coreos/dex/db"
"github.com/coreos/dex/schema/adminschema" "github.com/coreos/dex/schema/adminschema"
"github.com/coreos/dex/user" "github.com/coreos/dex/user"
"github.com/coreos/dex/user/manager" "github.com/coreos/dex/user/manager"
@ -11,22 +16,25 @@ import (
// AdminAPI provides the logic necessary to implement the Admin API. // AdminAPI provides the logic necessary to implement the Admin API.
type AdminAPI struct { type AdminAPI struct {
userManager *manager.UserManager userManager *manager.UserManager
userRepo user.UserRepo userRepo user.UserRepo
passwordInfoRepo user.PasswordInfoRepo passwordInfoRepo user.PasswordInfoRepo
localConnectorID string clientIdentityRepo client.ClientIdentityRepo
localConnectorID string
} }
func NewAdminAPI(userManager *manager.UserManager, userRepo user.UserRepo, pwiRepo user.PasswordInfoRepo, localConnectorID string) *AdminAPI { // TODO(ericchiang): Swap the DbMap for a storage interface. See #278
func NewAdminAPI(dbMap *gorp.DbMap, userManager *manager.UserManager, localConnectorID string) *AdminAPI {
if localConnectorID == "" { if localConnectorID == "" {
panic("must specify non-blank localConnectorID") panic("must specify non-blank localConnectorID")
} }
return &AdminAPI{ return &AdminAPI{
userManager: userManager, userManager: userManager,
userRepo: userRepo, userRepo: db.NewUserRepo(dbMap),
passwordInfoRepo: pwiRepo, passwordInfoRepo: db.NewPasswordInfoRepo(dbMap),
localConnectorID: localConnectorID, clientIdentityRepo: db.NewClientIdentityRepo(dbMap),
localConnectorID: localConnectorID,
} }
} }
@ -108,6 +116,27 @@ func (a *AdminAPI) GetState() (adminschema.State, error) {
return state, nil return state, nil
} }
type ClientRegistrationRequest struct {
IsAdmin bool `json:"isAdmin"`
Client oidc.ClientMetadata `json:"client"`
}
func (a *AdminAPI) CreateClient(req ClientRegistrationRequest) (oidc.ClientRegistrationResponse, error) {
if err := req.Client.Valid(); err != nil {
return oidc.ClientRegistrationResponse{}, mapError(err)
}
// metadata is guarenteed to have at least one redirect_uri by earlier validation.
id, err := oidc.GenClientID(req.Client.RedirectURIs[0].Host)
if err != nil {
return oidc.ClientRegistrationResponse{}, mapError(err)
}
c, err := a.clientIdentityRepo.New(id, req.Client, req.IsAdmin)
if err != nil {
return oidc.ClientRegistrationResponse{}, mapError(err)
}
return oidc.ClientRegistrationResponse{ClientID: c.ID, ClientSecret: c.Secret, ClientMetadata: req.Client}, nil
}
func mapError(e error) error { func mapError(e error) error {
if mapped, ok := errorMap[e]; ok { if mapped, ok := errorMap[e]; ok {
return mapped(e) return mapped(e)

View file

@ -69,7 +69,7 @@ func makeTestFixtures() *testFixtures {
}() }()
f.mgr = manager.NewUserManager(f.ur, f.pwr, ccr, db.TransactionFactory(dbMap), manager.ManagerOptions{}) f.mgr = manager.NewUserManager(f.ur, f.pwr, ccr, db.TransactionFactory(dbMap), manager.ManagerOptions{})
f.adAPI = NewAdminAPI(f.mgr, f.ur, f.pwr, "local") f.adAPI = NewAdminAPI(dbMap, f.mgr, "local")
return f return f
} }

View file

@ -113,13 +113,13 @@ func main() {
time.Sleep(sleep) time.Sleep(sleep)
} }
} }
userRepo := db.NewUserRepo(dbc) userRepo := db.NewUserRepo(dbc)
pwiRepo := db.NewPasswordInfoRepo(dbc) pwiRepo := db.NewPasswordInfoRepo(dbc)
connCfgRepo := db.NewConnectorConfigRepo(dbc) connCfgRepo := db.NewConnectorConfigRepo(dbc)
userManager := manager.NewUserManager(userRepo, userManager := manager.NewUserManager(userRepo,
pwiRepo, connCfgRepo, db.TransactionFactory(dbc), manager.ManagerOptions{}) pwiRepo, connCfgRepo, db.TransactionFactory(dbc), manager.ManagerOptions{})
adminAPI := admin.NewAdminAPI(userManager, userRepo, pwiRepo, *localConnectorID)
adminAPI := admin.NewAdminAPI(dbc, userManager, *localConnectorID)
kRepo, err := db.NewPrivateKeySetRepo(dbc, *useOldFormat, keySecrets.BytesSlice()...) kRepo, err := db.NewPrivateKeySetRepo(dbc, *useOldFormat, keySecrets.BytesSlice()...)
if err != nil { if err != nil {
log.Fatalf(err.Error()) log.Fatalf(err.Error())

View file

@ -1,8 +1,10 @@
package integration package integration
import ( import (
"errors"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/url"
"testing" "testing"
"github.com/kylelemons/godebug/pretty" "github.com/kylelemons/godebug/pretty"
@ -12,6 +14,7 @@ import (
"github.com/coreos/dex/schema/adminschema" "github.com/coreos/dex/schema/adminschema"
"github.com/coreos/dex/server" "github.com/coreos/dex/server"
"github.com/coreos/dex/user" "github.com/coreos/dex/user"
"github.com/coreos/go-oidc/oidc"
) )
const ( const (
@ -74,10 +77,10 @@ func (a *adminAPITransport) RoundTrip(r *http.Request) (*http.Response, error) {
func makeAdminAPITestFixtures() *adminAPITestFixtures { func makeAdminAPITestFixtures() *adminAPITestFixtures {
f := &adminAPITestFixtures{} f := &adminAPITestFixtures{}
ur, pwr, um := makeUserObjects(adminUsers, adminPasswords) dbMap, ur, pwr, um := makeUserObjects(adminUsers, adminPasswords)
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(dbMap, um, "local")
f.adSrv = server.NewAdminServer(f.adAPI, nil, adminAPITestSecret) 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{
@ -252,6 +255,52 @@ func TestCreateAdmin(t *testing.T) {
} }
} }
func TestCreateClient(t *testing.T) {
tests := []struct {
client oidc.ClientMetadata
wantError bool
}{
{
client: oidc.ClientMetadata{},
wantError: true,
},
{
client: oidc.ClientMetadata{
RedirectURIs: []url.URL{
{Scheme: "https", Host: "auth.example.com", Path: "/"},
},
},
},
}
for i, tt := range tests {
err := func() error {
f := makeAdminAPITestFixtures()
req := &adminschema.ClientCreateRequestClient{}
for _, redirectURI := range tt.client.RedirectURIs {
req.Redirect_uris = append(req.Redirect_uris, redirectURI.String())
}
resp, err := f.adClient.Client.Create(&adminschema.ClientCreateRequest{Client: req}).Do()
if err != nil {
if tt.wantError {
return nil
}
return err
}
if resp.Client_id == "" {
return errors.New("no client id returned")
}
if resp.Client_secret == "" {
return errors.New("no client secret returned")
}
return nil
}()
if err != nil {
t.Errorf("case %d: %v", i, err)
}
}
}
func TestGetState(t *testing.T) { func TestGetState(t *testing.T) {
tests := []struct { tests := []struct {
addUsers []user.User addUsers []user.User

View file

@ -9,6 +9,7 @@ import (
"net/url" "net/url"
"github.com/coreos/go-oidc/key" "github.com/coreos/go-oidc/key"
"github.com/go-gorp/gorp"
"github.com/jonboulle/clockwork" "github.com/jonboulle/clockwork"
"github.com/coreos/dex/connector" "github.com/coreos/dex/connector"
@ -45,7 +46,9 @@ func (t *tokenHandlerTransport) RoundTrip(r *http.Request) (*http.Response, erro
return &resp, nil return &resp, nil
} }
func makeUserObjects(users []user.UserWithRemoteIdentities, passwords []user.PasswordInfo) (user.UserRepo, user.PasswordInfoRepo, *manager.UserManager) { // TODO(ericchiang): Replace DbMap with storage interface. See #278
func makeUserObjects(users []user.UserWithRemoteIdentities, passwords []user.PasswordInfo) (*gorp.DbMap, user.UserRepo, user.PasswordInfoRepo, *manager.UserManager) {
dbMap := db.NewMemDB() dbMap := db.NewMemDB()
ur := func() user.UserRepo { ur := func() user.UserRepo {
repo, err := db.NewUserRepoFromUsers(dbMap, users) repo, err := db.NewUserRepoFromUsers(dbMap, users)
@ -73,5 +76,5 @@ func makeUserObjects(users []user.UserWithRemoteIdentities, passwords []user.Pas
um := manager.NewUserManager(ur, pwr, ccr, db.TransactionFactory(dbMap), manager.ManagerOptions{}) um := manager.NewUserManager(ur, pwr, ccr, db.TransactionFactory(dbMap), manager.ManagerOptions{})
um.Clock = clock um.Clock = clock
return ur, pwr, um return dbMap, ur, pwr, um
} }

View file

@ -99,7 +99,7 @@ var (
func makeUserAPITestFixtures() *userAPITestFixtures { func makeUserAPITestFixtures() *userAPITestFixtures {
f := &userAPITestFixtures{} f := &userAPITestFixtures{}
_, _, um := makeUserObjects(userUsers, userPasswords) _, _, _, um := makeUserObjects(userUsers, userPasswords)
cir := func() client.ClientIdentityRepo { cir := func() client.ClientIdentityRepo {
repo, err := db.NewClientIdentityRepoFromClients(db.NewMemDB(), []oidc.ClientIdentity{ repo, err := db.NewClientIdentityRepoFromClients(db.NewMemDB(), []oidc.ClientIdentity{

View file

@ -20,9 +20,10 @@ const (
) )
var ( var (
AdminGetEndpoint = addBasePath("/admin/:id") AdminGetEndpoint = addBasePath("/admin/:id")
AdminCreateEndpoint = addBasePath("/admin") AdminCreateEndpoint = addBasePath("/admin")
AdminGetStateEndpoint = addBasePath("/state") AdminGetStateEndpoint = addBasePath("/state")
AdminCreateClientEndpoint = addBasePath("/client")
) )
// AdminServer serves the admin API. // AdminServer serves the admin API.
@ -49,6 +50,7 @@ func (s *AdminServer) HTTPHandler() http.Handler {
r.GET(AdminGetEndpoint, s.getAdmin) r.GET(AdminGetEndpoint, s.getAdmin)
r.POST(AdminCreateEndpoint, s.createAdmin) r.POST(AdminCreateEndpoint, s.createAdmin)
r.GET(AdminGetStateEndpoint, s.getState) r.GET(AdminGetStateEndpoint, s.getState)
r.POST(AdminCreateClientEndpoint, s.createClient)
r.Handler("GET", httpPathHealth, s.checker) r.Handler("GET", httpPathHealth, s.checker)
r.HandlerFunc("GET", httpPathDebugVars, health.ExpvarHandler) r.HandlerFunc("GET", httpPathDebugVars, health.ExpvarHandler)
@ -113,6 +115,21 @@ func (s *AdminServer) getState(w http.ResponseWriter, r *http.Request, ps httpro
writeResponseWithBody(w, http.StatusOK, state) writeResponseWithBody(w, http.StatusOK, state)
} }
func (s *AdminServer) createClient(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
var req admin.ClientRegistrationRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeInvalidRequest(w, "cannot parse JSON body")
return
}
resp, err := s.adminAPI.CreateClient(req)
if err != nil {
s.writeError(w, err)
return
}
writeResponseWithBody(w, http.StatusOK, &resp)
}
func (s *AdminServer) writeError(w http.ResponseWriter, err error) { func (s *AdminServer) writeError(w http.ResponseWriter, err error) {
log.Errorf("Error calling admin API: %v: ", err) log.Errorf("Error calling admin API: %v: ", err)
if adminErr, ok := err.(admin.Error); ok { if adminErr, ok := err.(admin.Error); ok {