SAML Database Support

Partially addresses #1456. This PR provides datastore support for SSO by creating a new entity IdentityProvider. This entity is an abstraction of the SAML IdentityProvider and contains the data needed to perform SAML authentication.
This commit is contained in:
John Murphy 2017-04-12 15:42:10 -05:00 committed by GitHub
parent 40610e508f
commit 789596a78e
11 changed files with 363 additions and 0 deletions

View File

@ -0,0 +1,81 @@
package datastore
import (
"testing"
"github.com/kolide/kolide/server/kolide"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func testIdentityProvider(t *testing.T, ds kolide.Datastore) {
if ds.Name() == "inmem" {
t.Skip("imem is being deprecated")
}
idps := []*kolide.IdentityProvider{
&kolide.IdentityProvider{
SingleSignOnURL: "https://idp1.com/sso",
IssuerURI: "http://idp1.com/issuer/xyz123",
Certificate: "DEADBEEFXXXXX12344",
Name: "idp1",
ImageURL: "https://idp1.com/logo.png",
},
&kolide.IdentityProvider{
SingleSignOnURL: "https://idp2.com/sso",
IssuerURI: "http://idp2.com/issuer/xyz123",
Certificate: "DEADBEEFXXXXX12344",
Name: "idp2",
ImageURL: "https://idp2.com/logo.png",
},
&kolide.IdentityProvider{
SingleSignOnURL: "https://idp3.com/sso",
IssuerURI: "http://idp3.com/issuer/xyz123",
Certificate: "DEADBEEFXXXXX12344",
Name: "idp3",
ImageURL: "https://idp3.com/logo.png",
},
}
var err error
for i, idp := range idps {
idps[i], err = ds.NewIdentityProvider(*idp)
require.Nil(t, err)
require.NotEqual(t, 0, idp.ID, "id assignment")
}
// duplicate name not allowed
_, err = ds.NewIdentityProvider(*idps[0])
assert.NotNil(t, err)
// test get
idp, err := ds.IdentityProvider(idps[0].ID)
require.Nil(t, err)
require.NotNil(t, idp)
require.Equal(t, "idp1", idp.Name)
// test update
idp.ImageURL = "https://idpnew.com/logo.png"
idp.SingleSignOnURL = "https://idpnew.com/sso"
idp.IssuerURI = "https://idpnew.com/issuer"
idp.Certificate = "123456789"
idp.Name = "idpnew"
err = ds.SaveIdentityProvider(*idp)
require.Nil(t, err)
upd, err := ds.IdentityProvider(idp.ID)
require.Nil(t, err)
require.NotNil(t, upd)
assert.Equal(t, idp.ImageURL, upd.ImageURL)
assert.Equal(t, idp.SingleSignOnURL, upd.SingleSignOnURL)
assert.Equal(t, idp.IssuerURI, upd.IssuerURI)
assert.Equal(t, idp.Certificate, upd.Certificate)
assert.Equal(t, idp.Name, upd.Name)
// test list
results, err := ds.ListIdentityProviders()
require.Nil(t, err)
require.NotNil(t, results)
assert.Len(t, results, 3)
// test delete
err = ds.DeleteIdentityProvider(results[0].ID)
assert.Nil(t, err)
err = ds.DeleteIdentityProvider(results[0].ID)
assert.NotNil(t, err)
results, err = ds.ListIdentityProviders()
require.Nil(t, err)
assert.NotNil(t, results, 2)
}

View File

@ -78,4 +78,5 @@ var testFunctions = [...]func(*testing.T, kolide.Datastore){
testUnicode, testUnicode,
testCountHostsInTargets, testCountHostsInTargets,
testResetOptions, testResetOptions,
testIdentityProvider,
} }

View File

@ -0,0 +1,25 @@
package inmem
import (
"github.com/kolide/kolide/server/kolide"
)
func (d *Datastore) NewIdentityProvider(idp kolide.IdentityProvider) (*kolide.IdentityProvider, error) {
panic("inmem is being deprecated")
}
func (d *Datastore) SaveIdentityProvider(idb kolide.IdentityProvider) error {
panic("inmem is being deprecated")
}
func (d *Datastore) IdentityProvider(id uint) (*kolide.IdentityProvider, error) {
panic("inmem is being deprecated")
}
func (d *Datastore) DeleteIdentityProvider(id uint) error {
panic("inmem is being deprecated")
}
func (d *Datastore) ListIdentityProviders() ([]kolide.IdentityProvider, error) {
panic("inmem is being deprecated")
}

View File

@ -0,0 +1,91 @@
package mysql
import (
"database/sql"
"github.com/kolide/kolide/server/kolide"
"github.com/pkg/errors"
)
func (d *Datastore) NewIdentityProvider(idp kolide.IdentityProvider) (*kolide.IdentityProvider, error) {
query := `
INSERT INTO identity_providers (
sso_url,
issuer_uri,
cert,
name,
image_url
)
VALUES ( ?, ?, ?, ?, ? )
`
result, err := d.db.Exec(query, idp.SingleSignOnURL, idp.IssuerURI, idp.Certificate, idp.Name, idp.ImageURL)
if err != nil {
return nil, errors.Wrap(err, "creating identity provider")
}
id, err := result.LastInsertId()
if err != nil {
return nil, errors.Wrap(err, "retrieving id for new identity provider")
}
idp.ID = uint(id)
return &idp, nil
}
func (d *Datastore) SaveIdentityProvider(idp kolide.IdentityProvider) error {
query := `
UPDATE identity_providers
SET
sso_url = ?,
issuer_uri = ?,
cert = ?,
name = ?,
image_url = ?
WHERE id = ?
`
result, err := d.db.Exec(query, idp.SingleSignOnURL, idp.IssuerURI, idp.Certificate,
idp.Name, idp.ImageURL, idp.ID)
if err != nil {
return errors.Wrap(err, "updating identity provider")
}
rows, err := result.RowsAffected()
if err != nil {
return errors.Wrap(err, "fetching updated row count for identity provider")
}
if rows == 0 {
return notFound("IdentityProvider").WithID(idp.ID)
}
return nil
}
func (d *Datastore) IdentityProvider(id uint) (*kolide.IdentityProvider, error) {
query := `
SELECT *
FROM identity_providers
WHERE id = ? AND NOT deleted
`
var idp kolide.IdentityProvider
err := d.db.Get(&idp, query, id)
if err == sql.ErrNoRows {
return nil, notFound("IdentityProvider").WithID(id)
}
if err != nil {
return nil, errors.Wrap(err, "selecting identity provider")
}
return &idp, nil
}
func (d *Datastore) DeleteIdentityProvider(id uint) error {
return d.deleteEntity("identity_providers", id)
}
func (d *Datastore) ListIdentityProviders() ([]kolide.IdentityProvider, error) {
query := `
SELECT *
FROM identity_providers
WHERE NOT deleted
`
var idps []kolide.IdentityProvider
if err := d.db.Select(&idps, query); err != nil {
return nil, errors.Wrap(err, "listing identity providers")
}
return idps, nil
}

View File

@ -0,0 +1,34 @@
package tables
import (
"database/sql"
)
func init() {
MigrationClient.AddMigration(Up_20170411155225, Down_20170411155225)
}
func Up_20170411155225(tx *sql.Tx) error {
statement :=
"CREATE TABLE `identity_providers` ( " +
"`id` int(11) NOT NULL AUTO_INCREMENT, " +
"`sso_url` varchar(1024) NOT NULL DEFAULT '', " +
"`issuer_uri` varchar(1024) NOT NULL DEFAULT '', " +
"`cert` text NOT NULL, " +
"`name` varchar(128) NOT NULL DEFAULT '', " +
"`image_url` varchar(1024) NOT NULL DEFAULT '', " +
"`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
"`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
"`deleted_at` timestamp NULL DEFAULT NULL, " +
"`deleted` tinyint(1) NOT NULL DEFAULT FALSE, " +
"PRIMARY KEY (`id`), " +
"UNIQUE KEY `idx_unique_identity_providers_name` (`name`) USING BTREE " +
") ENGINE=InnoDB DEFAULT CHARSET=utf8;"
_, err := tx.Exec(statement)
return err
}
func Down_20170411155225(tx *sql.Tx) error {
_, err := tx.Exec("DROP TABLE IF EXISTS `identity_providers`;")
return err
}

View File

@ -19,6 +19,7 @@ type Datastore interface {
FileIntegrityMonitoringStore FileIntegrityMonitoringStore
YARAStore YARAStore
LicenseStore LicenseStore
IdentityProviderStore
Name() string Name() string
Drop() error Drop() error
// MigrateTables creates and migrates the table schemas // MigrateTables creates and migrates the table schemas

View File

@ -0,0 +1,62 @@
package kolide
import "context"
// IdentityProviderStore exposes methods to persist IdentityProviders.
// IdentityProvider is an entity used for single sign on.
type IdentityProviderStore interface {
// NewIdentityProvider creates a new IdentityProvider.
NewIdentityProvider(idp IdentityProvider) (*IdentityProvider, error)
// SaveIdentityProvider saves changes to an IdentityProvider.
SaveIdentityProvider(idb IdentityProvider) error
// IdentityProvider retrieves an IdentityProvider identified by id.
IdentityProvider(id uint) (*IdentityProvider, error)
// DeleteIdentityProvider soft deletes an IdentityProvider
DeleteIdentityProvider(id uint) error
// ListIdentityProviders returns all IdentityProvider entities
ListIdentityProviders() ([]IdentityProvider, error)
}
// IdentityProvider represents a SAML identity provider.
type IdentityProvider struct {
UpdateCreateTimestamps
DeleteFields
ID uint `json:"id"`
// SingleSignOnURL is the URL for the identity provider.
SingleSignOnURL string `json:"sso_url" db:"sso_url"`
// IssuerURI identity provider issuer
IssuerURI string `json:"issuer_uri" db:"issuer_uri"`
// Certificate is the identity provider's public certificate.
Certificate string `json:"cert" db:"cert"`
// Name is the descriptive name for the identity provider that will
// be displayed in the UI.
Name string `json:"name"`
// ImageURL is a link to an icon that will be displayed on the SSO
// button for a particular identity provider.
ImageURL string `json:"image_url" db:"image_url"`
}
// IdentityProviderPayload user to update one or more fields of an IdentityProvider
// by supplying values that correspond to fields that will be changed.
type IdentityProviderPayload struct {
SingleSignOnURL *string `json:"sso_url"`
IssuerURI *string `json:"issuer_uri"`
Certificate *string `json:"cert"`
Name *string `json:"name"`
ImageURL *string `json:"image_url"`
}
// IdentityProviderService exposes methods to manage IdentityProvider entities
type IdentityProviderService interface {
// NewIdentityProvider creates a IdentityProvider
NewIdentityProvider(ctx context.Context, payload IdentityProviderPayload) (*IdentityProvider, error)
// SaveIdentityProvider is used to modify an existing IdentityProvider. Nonnil
// fields in the payload argument will be changed for an existing IdentityProvider
ModifyIdentityProvider(ctx context.Context, id uint, payload IdentityProviderPayload) (*IdentityProvider, error)
// GetIdentityProvider retrieves an IdentityProvider given it's ID.
GetIdentityProvider(ctx context.Context, id uint) (*IdentityProvider, error)
// DeleteIdentityProvider removes an IdentityProvider
DeleteIdentityProvider(ctx context.Context, id uint) error
// ListIdentityProviders returns a list of all IdentityProvider entities
ListIdentityProviders(ctx context.Context, id uint) ([]IdentityProvider, error)
}

View File

@ -33,6 +33,9 @@ type SessionStore interface {
} }
type SessionService interface { type SessionService interface {
// SSOLogin handles creating a session for a user who is authenticated by
// a SAML identity provider, returning the user and a token on success
SSOLogin(ctx context.Context, userId string) (*User, string, error)
Login(ctx context.Context, username, password string) (user *User, token string, err error) Login(ctx context.Context, username, password string) (user *User, token string, err error)
Logout(ctx context.Context) (err error) Logout(ctx context.Context) (err error)
DestroySession(ctx context.Context) (err error) DestroySession(ctx context.Context) (err error)

View File

@ -9,6 +9,7 @@ package mock
//go:generate mockimpl -o datastore_options.go "s *OptionStore" "kolide.OptionStore" //go:generate mockimpl -o datastore_options.go "s *OptionStore" "kolide.OptionStore"
//go:generate mockimpl -o datastore_packs.go "s *PackStore" "kolide.PackStore" //go:generate mockimpl -o datastore_packs.go "s *PackStore" "kolide.PackStore"
//go:generate mockimpl -o datastore_hosts.go "s *HostStore" "kolide.HostStore" //go:generate mockimpl -o datastore_hosts.go "s *HostStore" "kolide.HostStore"
//go:generate mockimpl -o datastore_identity_providers.go "s *IdentityProviderStore" "kolide.IdentityProviderStore"
import "github.com/kolide/kolide/server/kolide" import "github.com/kolide/kolide/server/kolide"
@ -32,6 +33,7 @@ type Store struct {
OptionStore OptionStore
PackStore PackStore
UserStore UserStore
IdentityProviderStore
} }
func (m *Store) Drop() error { func (m *Store) Drop() error {

View File

@ -0,0 +1,59 @@
// Automatically generated by mockimpl. DO NOT EDIT!
package mock
import "github.com/kolide/kolide/server/kolide"
var _ kolide.IdentityProviderStore = (*IdentityProviderStore)(nil)
type NewIdentityProviderFunc func(idp kolide.IdentityProvider) (*kolide.IdentityProvider, error)
type SaveIdentityProviderFunc func(idb kolide.IdentityProvider) error
type IdentityProviderFunc func(id uint) (*kolide.IdentityProvider, error)
type DeleteIdentityProviderFunc func(id uint) error
type ListIdentityProvidersFunc func() ([]kolide.IdentityProvider, error)
type IdentityProviderStore struct {
NewIdentityProviderFunc NewIdentityProviderFunc
NewIdentityProviderFuncInvoked bool
SaveIdentityProviderFunc SaveIdentityProviderFunc
SaveIdentityProviderFuncInvoked bool
IdentityProviderFunc IdentityProviderFunc
IdentityProviderFuncInvoked bool
DeleteIdentityProviderFunc DeleteIdentityProviderFunc
DeleteIdentityProviderFuncInvoked bool
ListIdentityProvidersFunc ListIdentityProvidersFunc
ListIdentityProvidersFuncInvoked bool
}
func (s *IdentityProviderStore) NewIdentityProvider(idp kolide.IdentityProvider) (*kolide.IdentityProvider, error) {
s.NewIdentityProviderFuncInvoked = true
return s.NewIdentityProviderFunc(idp)
}
func (s *IdentityProviderStore) SaveIdentityProvider(idb kolide.IdentityProvider) error {
s.SaveIdentityProviderFuncInvoked = true
return s.SaveIdentityProviderFunc(idb)
}
func (s *IdentityProviderStore) IdentityProvider(id uint) (*kolide.IdentityProvider, error) {
s.IdentityProviderFuncInvoked = true
return s.IdentityProviderFunc(id)
}
func (s *IdentityProviderStore) DeleteIdentityProvider(id uint) error {
s.DeleteIdentityProviderFuncInvoked = true
return s.DeleteIdentityProviderFunc(id)
}
func (s *IdentityProviderStore) ListIdentityProviders() ([]kolide.IdentityProvider, error) {
s.ListIdentityProvidersFuncInvoked = true
return s.ListIdentityProvidersFunc()
}

View File

@ -13,6 +13,10 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
) )
func (svc service) SSOLogin(ctx context.Context, userId string) (*kolide.User, string, error) {
return nil, "", errors.New("not implemented")
}
func (svc service) Login(ctx context.Context, username, password string) (*kolide.User, string, error) { func (svc service) Login(ctx context.Context, username, password string) (*kolide.User, string, error) {
user, err := svc.userByEmailOrUsername(username) user, err := svc.userByEmailOrUsername(username)
if _, ok := err.(kolide.NotFoundError); ok { if _, ok := err.(kolide.NotFoundError); ok {