diff --git a/server/datastore/datastore_identity_providers_test.go b/server/datastore/datastore_identity_providers_test.go new file mode 100644 index 000000000..572b70f1a --- /dev/null +++ b/server/datastore/datastore_identity_providers_test.go @@ -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) +} diff --git a/server/datastore/datastore_test.go b/server/datastore/datastore_test.go index 8c1523d77..2629570d0 100644 --- a/server/datastore/datastore_test.go +++ b/server/datastore/datastore_test.go @@ -78,4 +78,5 @@ var testFunctions = [...]func(*testing.T, kolide.Datastore){ testUnicode, testCountHostsInTargets, testResetOptions, + testIdentityProvider, } diff --git a/server/datastore/inmem/identity_providers.go b/server/datastore/inmem/identity_providers.go new file mode 100644 index 000000000..04b4a2423 --- /dev/null +++ b/server/datastore/inmem/identity_providers.go @@ -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") +} diff --git a/server/datastore/mysql/identity_providers.go b/server/datastore/mysql/identity_providers.go new file mode 100644 index 000000000..7120c0950 --- /dev/null +++ b/server/datastore/mysql/identity_providers.go @@ -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 +} diff --git a/server/datastore/mysql/migrations/tables/20170411155225_CreateTableIdentityProviders.go b/server/datastore/mysql/migrations/tables/20170411155225_CreateTableIdentityProviders.go new file mode 100644 index 000000000..9339db33c --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20170411155225_CreateTableIdentityProviders.go @@ -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 +} diff --git a/server/kolide/datastore.go b/server/kolide/datastore.go index de43e5e70..791eff87e 100644 --- a/server/kolide/datastore.go +++ b/server/kolide/datastore.go @@ -19,6 +19,7 @@ type Datastore interface { FileIntegrityMonitoringStore YARAStore LicenseStore + IdentityProviderStore Name() string Drop() error // MigrateTables creates and migrates the table schemas diff --git a/server/kolide/identity_providers.go b/server/kolide/identity_providers.go new file mode 100644 index 000000000..271bac19a --- /dev/null +++ b/server/kolide/identity_providers.go @@ -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) +} diff --git a/server/kolide/sessions.go b/server/kolide/sessions.go index ef1c50afc..0271d53bb 100644 --- a/server/kolide/sessions.go +++ b/server/kolide/sessions.go @@ -33,6 +33,9 @@ type SessionStore 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) Logout(ctx context.Context) (err error) DestroySession(ctx context.Context) (err error) diff --git a/server/mock/datastore.go b/server/mock/datastore.go index a5800c976..36b615a18 100644 --- a/server/mock/datastore.go +++ b/server/mock/datastore.go @@ -9,6 +9,7 @@ package mock //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_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" @@ -32,6 +33,7 @@ type Store struct { OptionStore PackStore UserStore + IdentityProviderStore } func (m *Store) Drop() error { diff --git a/server/mock/datastore_identity_providers.go b/server/mock/datastore_identity_providers.go new file mode 100644 index 000000000..f906afcb1 --- /dev/null +++ b/server/mock/datastore_identity_providers.go @@ -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() +} diff --git a/server/service/service_sessions.go b/server/service/service_sessions.go index 62bc38934..5d403c412 100644 --- a/server/service/service_sessions.go +++ b/server/service/service_sessions.go @@ -13,6 +13,10 @@ import ( "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) { user, err := svc.userByEmailOrUsername(username) if _, ok := err.(kolide.NotFoundError); ok {