forked from mystiq/dex
*: add client registration endpoint to admin API
This commit is contained in:
parent
0445da2dfe
commit
b10645f58d
7 changed files with 119 additions and 21 deletions
49
admin/api.go
49
admin/api.go
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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{
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue