diff --git a/admin/api.go b/admin/api.go index 2329c3ba..d65c71f9 100644 --- a/admin/api.go +++ b/admin/api.go @@ -4,6 +4,11 @@ package admin import ( "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/user" "github.com/coreos/dex/user/manager" @@ -11,22 +16,25 @@ import ( // AdminAPI provides the logic necessary to implement the Admin API. type AdminAPI struct { - userManager *manager.UserManager - userRepo user.UserRepo - passwordInfoRepo user.PasswordInfoRepo - localConnectorID string + userManager *manager.UserManager + userRepo user.UserRepo + passwordInfoRepo user.PasswordInfoRepo + 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 == "" { panic("must specify non-blank localConnectorID") } - return &AdminAPI{ - userManager: userManager, - userRepo: userRepo, - passwordInfoRepo: pwiRepo, - localConnectorID: localConnectorID, + userManager: userManager, + userRepo: db.NewUserRepo(dbMap), + passwordInfoRepo: db.NewPasswordInfoRepo(dbMap), + clientIdentityRepo: db.NewClientIdentityRepo(dbMap), + localConnectorID: localConnectorID, } } @@ -108,6 +116,27 @@ func (a *AdminAPI) GetState() (adminschema.State, error) { 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 { if mapped, ok := errorMap[e]; ok { return mapped(e) diff --git a/admin/api_test.go b/admin/api_test.go index 4409e2b7..165d8ff5 100644 --- a/admin/api_test.go +++ b/admin/api_test.go @@ -69,7 +69,7 @@ func makeTestFixtures() *testFixtures { }() 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 } diff --git a/cmd/dex-overlord/main.go b/cmd/dex-overlord/main.go index 1d3eb5de..8dad8aa0 100644 --- a/cmd/dex-overlord/main.go +++ b/cmd/dex-overlord/main.go @@ -113,13 +113,13 @@ func main() { time.Sleep(sleep) } } - userRepo := db.NewUserRepo(dbc) pwiRepo := db.NewPasswordInfoRepo(dbc) connCfgRepo := db.NewConnectorConfigRepo(dbc) userManager := manager.NewUserManager(userRepo, 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()...) if err != nil { log.Fatalf(err.Error()) diff --git a/integration/admin_api_test.go b/integration/admin_api_test.go index 337eccec..861e9788 100644 --- a/integration/admin_api_test.go +++ b/integration/admin_api_test.go @@ -1,8 +1,10 @@ package integration import ( + "errors" "net/http" "net/http/httptest" + "net/url" "testing" "github.com/kylelemons/godebug/pretty" @@ -12,6 +14,7 @@ import ( "github.com/coreos/dex/schema/adminschema" "github.com/coreos/dex/server" "github.com/coreos/dex/user" + "github.com/coreos/go-oidc/oidc" ) const ( @@ -74,10 +77,10 @@ func (a *adminAPITransport) RoundTrip(r *http.Request) (*http.Response, error) { func makeAdminAPITestFixtures() *adminAPITestFixtures { f := &adminAPITestFixtures{} - ur, pwr, um := makeUserObjects(adminUsers, adminPasswords) + dbMap, ur, pwr, um := makeUserObjects(adminUsers, adminPasswords) f.ur = ur 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.hSrv = httptest.NewServer(f.adSrv.HTTPHandler()) 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) { tests := []struct { addUsers []user.User diff --git a/integration/common_test.go b/integration/common_test.go index f7fe429b..aeb71391 100644 --- a/integration/common_test.go +++ b/integration/common_test.go @@ -9,6 +9,7 @@ import ( "net/url" "github.com/coreos/go-oidc/key" + "github.com/go-gorp/gorp" "github.com/jonboulle/clockwork" "github.com/coreos/dex/connector" @@ -45,7 +46,9 @@ func (t *tokenHandlerTransport) RoundTrip(r *http.Request) (*http.Response, erro 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() ur := func() user.UserRepo { 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.Clock = clock - return ur, pwr, um + return dbMap, ur, pwr, um } diff --git a/integration/user_api_test.go b/integration/user_api_test.go index 687d32f5..f2f4758f 100644 --- a/integration/user_api_test.go +++ b/integration/user_api_test.go @@ -99,7 +99,7 @@ var ( func makeUserAPITestFixtures() *userAPITestFixtures { f := &userAPITestFixtures{} - _, _, um := makeUserObjects(userUsers, userPasswords) + _, _, _, um := makeUserObjects(userUsers, userPasswords) cir := func() client.ClientIdentityRepo { repo, err := db.NewClientIdentityRepoFromClients(db.NewMemDB(), []oidc.ClientIdentity{ diff --git a/server/admin.go b/server/admin.go index 8295dfe6..21bd28d5 100644 --- a/server/admin.go +++ b/server/admin.go @@ -20,9 +20,10 @@ const ( ) var ( - AdminGetEndpoint = addBasePath("/admin/:id") - AdminCreateEndpoint = addBasePath("/admin") - AdminGetStateEndpoint = addBasePath("/state") + AdminGetEndpoint = addBasePath("/admin/:id") + AdminCreateEndpoint = addBasePath("/admin") + AdminGetStateEndpoint = addBasePath("/state") + AdminCreateClientEndpoint = addBasePath("/client") ) // AdminServer serves the admin API. @@ -49,6 +50,7 @@ func (s *AdminServer) HTTPHandler() http.Handler { r.GET(AdminGetEndpoint, s.getAdmin) r.POST(AdminCreateEndpoint, s.createAdmin) r.GET(AdminGetStateEndpoint, s.getState) + r.POST(AdminCreateClientEndpoint, s.createClient) r.Handler("GET", httpPathHealth, s.checker) 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) } +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) { log.Errorf("Error calling admin API: %v: ", err) if adminErr, ok := err.(admin.Error); ok {