diff --git a/.env.sample b/.env.sample index c85288a8a..540c702f5 100644 --- a/.env.sample +++ b/.env.sample @@ -19,15 +19,8 @@ SYSTEM_DB_PROFILER=stdout METRICS=1 METRICS_PASSWORD=metrics -AUTH_JWT_SECRET=123 +AUTH_JWT_SECRET=jwt-secret AUTH_JWT_DEBUG=1 -AUTH_OIDC_ISSUER=123 -AUTH_OIDC_REDIRECT_URL=213 -AUTH_OIDC_COOKIE_DOMAIN="" - -RBAC_AUTH=tenant:password -RBAC_TENANT=tenant -RBAC_BASE_URL=https://crust.example.org/pdp/ # Make sure you run `make mailhog.up` and visit localhost:8025. SMTP_HOST=localhost:1025 diff --git a/Gopkg.lock b/Gopkg.lock index d3a2c8367..b385f8bda 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -253,7 +253,7 @@ version = "v1.0.0" [[projects]] - digest = "1:c6a991538ddd77daaad1380ec85e1d7c6a0ae2bc2732b059ddb95f9a49569d3d" + digest = "1:eb5f06b8a308f49f2205386975ab854606f01784546996964ade50fc41156875" name = "github.com/markbates/goth" packages = [ ".", @@ -262,6 +262,7 @@ "providers/github", "providers/gplus", "providers/linkedin", + "providers/openidConnect", ] pruneopts = "UT" revision = "157987f620ff2fc5e1f6a1427a3685219fbf6ff4" @@ -554,6 +555,7 @@ "github.com/markbates/goth/providers/github", "github.com/markbates/goth/providers/gplus", "github.com/markbates/goth/providers/linkedin", + "github.com/markbates/goth/providers/openidConnect", "github.com/namsral/flag", "github.com/pkg/errors", "github.com/prometheus/client_golang/prometheus", diff --git a/crm/internal/service/permissions_test.go b/crm/internal/service/permissions_test.go index 8dfeb367a..9b9c2baae 100644 --- a/crm/internal/service/permissions_test.go +++ b/crm/internal/service/permissions_test.go @@ -27,7 +27,6 @@ func TestPermissions(t *testing.T) { user := &systemTypes.User{ Name: "John Crm Doe", Username: "johndoe", - SatosaID: "12345", } err := user.GeneratePassword("johndoe") NoError(t, err, "expected no error generating password, got %+v", err) diff --git a/docs/Authentication.md b/docs/Authentication.md new file mode 100644 index 000000000..0e7e8b753 --- /dev/null +++ b/docs/Authentication.md @@ -0,0 +1,78 @@ +# Authentication + +Crust support a fixed set of standard OAuth 2 authentication providers +(facebook, gplus, github and linkedin) and a arbitrary number of custom +issuers (over OpenID Connect). + +# Available settings + +Settings for external providers are stored under keys +`auth.external.providers..` and +`auth.external.providers.openid-connect..`. + +Prop is one of: `key`, `secret`, `enabled`. OIDC settings also have `issuer` prop. + +Example settings (`system-cli settings list --prefix=auth.external`): + + +``` +auth.external.callback-endpoint "http://system.api.local.crust.tech:3002/auth/external/%s/callback" +auth.external.enabled true +auth.external.providers.facebook.enabled true +auth.external.providers.facebook.key "24226007270326" +auth.external.providers.facebook.secret "7vtfXx213cfc125804a226afcae777fe47" +auth.external.providers.gplus true +auth.external.providers.gplus.enabled true +auth.external.providers.gplus.key "10629818561-7a8vr0avs47dqic43h2lkrurhr.apps.googleusercontent.com" +auth.external.providers.gplus.secret "bkHmIFdk2YvtfXx" +auth.external.providers.github.enabled false +auth.external.providers.github.key null +auth.external.providers.github.secret null +auth.external.providers.linkedin.enabled false +auth.external.providers.linkedin.key null +auth.external.providers.linkedin.secret null +auth.external.providers.openid-connect.didmos2.enabled true +auth.external.providers.openid-connect.didmos2.key "tXM2ouiovowzGabk" +auth.external.providers.openid-connect.didmos2.issuer "https://satosa.didmos.latest.crust.tech" +auth.external.providers.openid-connect.didmos2.secret "e1d68bfd7718468ba8fd36131f5176b1" +auth.external.redirect-url "http://system.api.local.crust.tech:3002/auth/external/%s/callback" +auth.external.session-store-secret "fCVFSRWjVEcoYuhXSf3f6zVWO1p38XEWz2yS8WH7wKDbvpxFrZq7zlEuiUTvk4QF" +``` + +# Changing settings + +Authentication settings can be changed in the administration (via the API) and with cli +command (`system-cli settings set `). Please bare in mind that values passed +to CLI tool must always be in raw JSON format. + +Changing values requires system service restart. + +On startup, you should see log entries similar to these: +``` +initializing external authentication providers (3) +external authentication provider "facebook" added +external authentication provider "gplus" added +external authentication provider "openid-connect.didmos2" added +``` + + +# OIDC Auto discovery/configuration + +Crust CLI comes with auto-discovery tool: +```bash +system-cli external-auth auto-discovery name url +``` + +```bash +system-cli external-auth auto-discovery didmos2 https://satosa.didmos.crust.example.tld +``` + +This will autodiscover and autoconfigure new OIDC provider. +If entry with this name already exists it will override it. + +Please note that this provider is disabled by default. + +To enable it, run: +```bash +system-cli settings key auth.external.providers.openid-connect.didmos2.enabled true +``` diff --git a/internal/config/oidc.go b/internal/config/oidc.go deleted file mode 100644 index 77183d724..000000000 --- a/internal/config/oidc.go +++ /dev/null @@ -1,60 +0,0 @@ -package config - -import ( - "github.com/namsral/flag" - "github.com/pkg/errors" -) - -type ( - OIDC struct { - Enabled bool - Issuer string - ClientID string - ClientSecret string - - RedirectURL string - AppURL string - - StateCookieDomain string - StateCookieExpiry int64 - } -) - -var oidc *OIDC - -func (c *OIDC) Validate() error { - if c == nil { - return nil - } - if c.Enabled == false { - return nil - } - if c.Issuer == "" { - return errors.New("OIDC Issuer not set for AUTH") - } - if c.RedirectURL == "" { - return errors.New("OIDC RedirectURL not set for AUTH") - } - if c.StateCookieDomain == "" { - return errors.New("OIDC CookieDomain not set") - } - - return nil -} - -func (*OIDC) Init(prefix ...string) *OIDC { - if oidc != nil { - return oidc - } - - oidc = new(OIDC) - flag.BoolVar(&oidc.Enabled, "auth-oidc-enabled", true, "OIDC enabled") - flag.StringVar(&oidc.Issuer, "auth-oidc-issuer", "", "OIDC Issuer") - flag.StringVar(&oidc.ClientID, "auth-oidc-client-id", "", "OIDC Client ID") - flag.StringVar(&oidc.ClientSecret, "auth-oidc-client-secret", "", "OIDC Client Secret") - flag.StringVar(&oidc.RedirectURL, "auth-oidc-redirect-url", "", "OIDC RedirectURL") - flag.StringVar(&oidc.AppURL, "auth-oidc-app-url", "", "OIDC AppURL") - flag.StringVar(&oidc.StateCookieDomain, "auth-oidc-cookie-domain", "", "JWT Cookie domain") - flag.Int64Var(&oidc.StateCookieExpiry, "auth-oidc-state-cookie-expiry", 15, "OIDC State cookie expiry in minutes") - return oidc -} diff --git a/internal/config/social.go b/internal/config/social.go deleted file mode 100644 index bfc9be52d..000000000 --- a/internal/config/social.go +++ /dev/null @@ -1,71 +0,0 @@ -package config - -import ( - "errors" - "strings" - - "github.com/namsral/flag" -) - -type ( - Social struct { - Enabled bool - - FacebookKey string - FacebookSecret string - GPlusKey string - GPlusSecret string - GitHubKey string - GitHubSecret string - LinkedInKey string - LinkedInSecret string - - Url string - - SessionStoreSecret string - SessionStoreExpiry int // seconds! - } -) - -var social *Social - -func (c *Social) Validate() error { - if c == nil { - return nil - } - - if c.Enabled == false { - return nil - } - - if c.SessionStoreSecret == "" { - return errors.New("Session store secret not set for SOCIAL") - } - - return nil -} - -func (*Social) Init(prefix ...string) *Social { - if social != nil { - return social - } - - b := func(name string, k, s *string) { - flag.StringVar(k, "auth-social-"+strings.ToLower(name)+"-key", "", name+" key") - flag.StringVar(s, "auth-social-"+strings.ToLower(name)+"-secret", "", name+" secret") - - } - - social = new(Social) - flag.BoolVar(&social.Enabled, "auth-social-enabled", false, "SocialAuth enabled") - - b("Facebook", &social.FacebookKey, &social.FacebookSecret) - b("GPlus", &social.GPlusKey, &social.GPlusSecret) - b("GitHub", &social.GitHubKey, &social.GitHubSecret) - b("LinkedIn", &social.LinkedInKey, &social.LinkedInSecret) - - flag.StringVar(&social.Url, "auth-social-url", "", "Base URL") - flag.StringVar(&social.SessionStoreSecret, "auth-social-session-store-secret", "", "Session store secret") - flag.IntVar(&social.SessionStoreExpiry, "auth-social-state-cookie-expiry", 60*15, "SocialAuth State cookie expiry in seconds") - return social -} diff --git a/internal/rand/rand.go b/internal/rand/rand.go new file mode 100644 index 000000000..1087d5a0e --- /dev/null +++ b/internal/rand/rand.go @@ -0,0 +1,41 @@ +package rand + +import ( + "math/rand" + "sync" + "time" +) + +// credits: https://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-golang + +const ( + letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + letterIdxBits = 6 // 6 bits to represent a letter index + letterIdxMask = 1<= 0; { + if remain == 0 { + cache, remain = randSrc.Int63(), letterIdxMax + } + if idx := int(cache & letterIdxMask); idx < len(letterBytes) { + b[i] = letterBytes[idx] + i-- + } + cache >>= letterIdxBits + remain-- + } + + return b +} diff --git a/system/cli/external-auth.go b/system/cli/external-auth.go new file mode 100644 index 000000000..d2a034002 --- /dev/null +++ b/system/cli/external-auth.go @@ -0,0 +1,51 @@ +package cli + +import ( + "context" + "os" + + "github.com/spf13/cobra" + + "github.com/crusttech/crust/internal/settings" + "github.com/crusttech/crust/system/internal/auth/external" +) + +// Will perform OpenID connect auto-configuration +func ExternalAuth(ctx context.Context, rootCmd *cobra.Command, settingsService settings.Service) { + exit := func(err error) { + if err != nil { + rootCmd.Printf("Error: %v\n", err) + os.Exit(1) + } else { + os.Exit(0) + } + } + + autoDiscover := &cobra.Command{ + Use: "auto-discovery [name] [url]", + Short: "Auto discovers new OIDC client", + Args: cobra.ExactArgs(2), + Run: func(cmd *cobra.Command, args []string) { + var name, url = args[0], args[1] + + if eas, err := external.ExternalAuthSettings(settingsService); err != nil { + exit(err) + } else if eap, err := external.RegisterNewOpenIdClient(ctx, eas, name, url); err != nil { + exit(err) + } else if vv, err := eap.MakeValueSet("openid-connect." + name); err != nil { + exit(err) + } else if err := settingsService.BulkSet(vv); err != nil { + exit(err) + } + }, + } + + settingsCmd := &cobra.Command{ + Use: "external-auth", + Short: "External authentication", + } + + settingsCmd.AddCommand(autoDiscover) + + rootCmd.AddCommand(settingsCmd) +} diff --git a/system/db/mysql/static.go b/system/db/mysql/static.go index 9c318bc91..3d2a47d12 100644 --- a/system/db/mysql/static.go +++ b/system/db/mysql/static.go @@ -3,4 +3,4 @@ // Package contains static assets. package mysql -var Asset = "PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1a\x00 \x0020180704080000.base.up.sqlUT\x05\x00\x01\x80Cm8-- all known organisations (crust instances) and our relation towards them\nCREATE TABLE organisations (\n id BIGINT UNSIGNED NOT NULL,\n fqn TEXT NOT NULL, -- fully qualified name of the organisation\n name TEXT NOT NULL, -- display name of the organisation\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n archived_at DATETIME NULL,\n deleted_at DATETIME NULL, -- organisation soft delete\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE settings (\n name VARCHAR(200) NOT NULL COMMENT 'Unique set of setting keys',\n value TEXT COMMENT 'Setting value',\n\n PRIMARY KEY (name)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\n-- Keeps all known users, home and external organisation\n-- changes are stored in audit log\nCREATE TABLE users (\n id BIGINT UNSIGNED NOT NULL,\n email TEXT NOT NULL,\n username TEXT NOT NULL,\n password TEXT NOT NULL,\n name TEXT NOT NULL,\n handle TEXT NOT NULL,\n meta JSON NOT NULL,\n satosa_id CHAR(36) NULL,\n\n rel_organisation BIGINT UNSIGNED NOT NULL,\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n suspended_at DATETIME NULL,\n deleted_at DATETIME NULL, -- user soft delete\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE UNIQUE INDEX uid_satosa ON users (satosa_id);\n\n-- Keeps all known teams\nCREATE TABLE teams (\n id BIGINT UNSIGNED NOT NULL,\n name TEXT NOT NULL, -- display name of the team\n handle TEXT NOT NULL, -- team handle string\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n archived_at DATETIME NULL,\n deleted_at DATETIME NULL, -- team soft delete\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\n-- Keeps team memberships\nCREATE TABLE team_members (\n rel_team BIGINT UNSIGNED NOT NULL REFERENCES organisation(id),\n rel_user BIGINT UNSIGNED NOT NULL,\n\n PRIMARY KEY (rel_team, rel_user)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08\xedzU\x8am \x00\x00m \x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00.\x00 \x0020181124181811.rename_and_prefix_tables.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE teams RENAME TO sys_team;\nALTER TABLE organisations RENAME TO sys_organisation;\nALTER TABLE team_members RENAME TO sys_team_member;\nALTER TABLE users RENAME TO sys_user;PK\x07\x08\xf2\xc4\x87\xe8\xb5\x00\x00\x00\xb5\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00-\x00 \x0020181125100429.add_user_kind_and_owner.up.sqlUT\x05\x00\x01\x80Cm8# add field to manage user type (bot support)\nALTER TABLE `sys_user` ADD `kind` VARCHAR(8) NOT NULL DEFAULT '' AFTER `handle`;\n\n# add field to manage \"ownership\" (get all bots created by user)\nALTER TABLE `sys_user` ADD `rel_user_id` BIGINT UNSIGNED NOT NULL AFTER `rel_organisation`, ADD INDEX (`rel_user_id`);\nPK\x07\x089\xa0\xdat8\x01\x00\x008\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00-\x00 \x0020181125153544.satosa_index_not_unique.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `sys_user` DROP INDEX `uid_satosa`, ADD INDEX `uid_satosa` (`satosa_id`) USING BTREE;PK\x07\x08\x0d\xf9\xd3ga\x00\x00\x00a\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00 \x0020181208140000.credentials.up.sqlUT\x05\x00\x01\x80Cm8-- Keeps all known users, home and external organisation\n-- changes are stored in audit log\nCREATE TABLE sys_credentials (\n id BIGINT UNSIGNED NOT NULL,\n rel_owner BIGINT UNSIGNED NOT NULL REFERENCES sys_users(id),\n label TEXT NOT NULL COMMENT 'something we can differentiate credentials by',\n kind VARCHAR(128) NOT NULL COMMENT 'hash, facebook, gplus, github, linkedin ...',\n credentials TEXT NOT NULL COMMENT 'crypted/hashed passwords, secrets, social profile ID',\n meta JSON NOT NULL,\n expires_at DATETIME NULL,\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n deleted_at DATETIME NULL, -- user soft delete\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE INDEX idx_owner ON sys_credentials (rel_owner);\nPK\x07\x08f\x1f\x08\xd0\x9a\x03\x00\x00\x9a\x03\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00)\x00 \x0020190103203201.users-password-null.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `sys_user` MODIFY `password` TEXT NULL;\nPK\x07\x080V\x13\x0f4\x00\x00\x004\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1b\x00 \x0020190116102104.rules.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE `sys_rules` (\n `rel_team` BIGINT UNSIGNED NOT NULL,\n `resource` VARCHAR(128) NOT NULL,\n `operation` VARCHAR(128) NOT NULL,\n `value` TINYINT(1) NOT NULL,\n\n PRIMARY KEY (`rel_team`, `resource`, `operation`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08\x05\x10[\x91\x05\x01\x00\x00\x05\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00)\x00 \x0020190221001051.rename-team-to-role.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE sys_team RENAME TO sys_role;\nALTER TABLE sys_team_member RENAME TO sys_role_member;\n\nALTER TABLE `sys_role_member` CHANGE COLUMN `rel_team` `rel_role` BIGINT UNSIGNED NOT NULL;\nALTER TABLE `sys_rules` CHANGE COLUMN `rel_team` `rel_role` BIGINT UNSIGNED NOT NULL;\nPK\x07\x08s-\x98\xd0\x13\x01\x00\x00\x13\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00,\x00 \x0020190226160000.system_roles_and_rules.up.sqlUT\x05\x00\x01\x80Cm8REPLACE INTO `sys_role` (`id`, `name`, `handle`) VALUES\n (1, 'Everyone', 'everyone'),\n (2, 'Administrators', 'admins');\n\n-- Value: Allow (2), Deny (1), Inherit(0),\nREPLACE INTO `sys_rules` (`rel_role`, `resource`, `operation`, `value`) VALUES\n-- Everyone\n (1, 'compose:*', 'access', 2),\n (1, 'messaging:*', 'access', 2),\n-- Admins\n (2, 'compose', 'namespace.create', 2),\n (2, 'compose', 'access', 2),\n (2, 'compose', 'grant', 2),\n (2, 'compose:namespace:*', 'page.create', 2),\n (2, 'compose:namespace:*', 'read', 2),\n (2, 'compose:namespace:*', 'update', 2),\n (2, 'compose:namespace:*', 'delete', 2),\n (2, 'compose:namespace:*', 'module.create', 2),\n (2, 'compose:namespace:*', 'chart.create', 2),\n (2, 'compose:namespace:*', 'trigger.create', 2),\n (2, 'compose:chart:*', 'read', 2),\n (2, 'compose:chart:*', 'update', 2),\n (2, 'compose:chart:*', 'delete', 2),\n (2, 'compose:trigger:*', 'read', 2),\n (2, 'compose:trigger:*', 'update', 2),\n (2, 'compose:trigger:*', 'delete', 2),\n (2, 'compose:page:*', 'read', 2),\n (2, 'compose:page:*', 'update', 2),\n (2, 'compose:page:*', 'delete', 2),\n (2, 'system', 'access', 2),\n (2, 'system', 'grant', 2),\n (2, 'system', 'organisation.create', 2),\n (2, 'system', 'role.create', 2),\n (2, 'system:organisation:*', 'access', 2),\n (2, 'system:role:*', 'read', 2),\n (2, 'system:role:*', 'update', 2),\n (2, 'system:role:*', 'delete', 2),\n (2, 'system:role:*', 'members.manage', 2),\n (2, 'messaging', 'access', 2),\n (2, 'messaging', 'grant', 2),\n (2, 'messaging', 'channel.public.create', 2),\n (2, 'messaging', 'channel.private.create', 2),\n (2, 'messaging', 'channel.group.create', 2),\n (2, 'messaging:channel:*', 'update', 2),\n (2, 'messaging:channel:*', 'leave', 2),\n (2, 'messaging:channel:*', 'read', 2),\n (2, 'messaging:channel:*', 'join', 2),\n (2, 'messaging:channel:*', 'delete', 2),\n (2, 'messaging:channel:*', 'undelete', 2),\n (2, 'messaging:channel:*', 'archive', 2),\n (2, 'messaging:channel:*', 'unarchive', 2),\n (2, 'messaging:channel:*', 'members.manage', 2),\n (2, 'messaging:channel:*', 'webhooks.manage', 2),\n (2, 'messaging:channel:*', 'attachments.manage', 2),\n (2, 'messaging:channel:*', 'message.attach', 2),\n (2, 'messaging:channel:*', 'message.update.all', 2),\n (2, 'messaging:channel:*', 'message.update.own', 2),\n (2, 'messaging:channel:*', 'message.delete.all', 2),\n (2, 'messaging:channel:*', 'message.delete.own', 2),\n (2, 'messaging:channel:*', 'message.embed', 2),\n (2, 'messaging:channel:*', 'message.send', 2),\n (2, 'messaging:channel:*', 'message.reply', 2),\n (2, 'messaging:channel:*', 'message.react', 2),\n (2, 'compose:module:*', 'read', 2),\n (2, 'compose:module:*', 'update', 2),\n (2, 'compose:module:*', 'delete', 2),\n (2, 'compose:module:*', 'record.create', 2),\n (2, 'compose:module:*', 'record.read', 2),\n (2, 'compose:module:*', 'record.update', 2),\n (2, 'compose:module:*', 'record.delete', 2);\nPK\x07\x08z\",\xe8t\x0b\x00\x00t\x0b\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\"\x00 \x0020190306205033.applications.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE sys_application (\n id BIGINT UNSIGNED NOT NULL,\n rel_owner BIGINT UNSIGNED NOT NULL REFERENCES sys_users(id),\n name TEXT NOT NULL COMMENT 'something we can differentiate application by',\n enabled BOOL NOT NULL,\n\n unify JSON NULL COMMENT 'unify specific settings',\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n deleted_at DATETIME NULL, -- user soft delete\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\n\nREPLACE INTO `sys_application` (`id`, `name`, `enabled`, `rel_owner`, `unify`) VALUES\n( 1, 'Crust Messaging', true, 0,\n '{\"logo\": \"/applications/crust.jpg\", \"icon\": \"/applications/crust_favicon.png\", \"url\": \"/messaging/\", \"listed\": true}'\n),\n( 2, 'Crust CRM', true, 0,\n '{\"logo\": \"/applications/crust.jpg\", \"icon\": \"/applications/crust_favicon.png\", \"url\": \"/crm/\", \"listed\": true}'\n),\n( 3, 'Crust Admin Area', true, 0,\n '{\"logo\": \"/applications/crust.jpg\", \"icon\": \"/applications/crust_favicon.png\", \"url\": \"/admin/\", \"listed\": true}'\n),\n( 4, 'Corteza Jitsi Bridge', true, 0,\n '{\"logo\": \"/applications/jitsi.png\", \"icon\": \"/applications/jitsi_icon.png\", \"url\": \"/bridge/jitsi/\", \"listed\": true}'\n),\n( 5, 'Google Maps', true, 0,\n '{\"logo\": \"/applications/google_maps.png\", \"icon\": \"/applications/google_maps_icon.png\", \"url\": \"/bridge/google-maps/\", \"listed\": true}'\n);\n\n-- Allow admin access to applications\nINSERT INTO `sys_rules` (`rel_role`, `resource`, `operation`, `value`) VALUES\n (1, 'system:application:*', 'read', 2),\n (2, 'system', 'application.create', 2),\n (2, 'system:application:*', 'read', 2),\n (2, 'system:application:*', 'update', 2),\n (2, 'system:application:*', 'delete', 2)\n;\nPK\x07\x08\xce\xaf\xb0\x07\x11\x07\x00\x00\x11\x07\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00 \x00migrations.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE IF NOT EXISTS `migrations` (\n `project` varchar(16) NOT NULL COMMENT 'sam, crm, ...',\n `filename` varchar(255) NOT NULL COMMENT 'yyyymmddHHMMSS.sql',\n `statement_index` int(11) NOT NULL COMMENT 'Statement number from SQL file',\n `status` TEXT NOT NULL COMMENT 'ok or full error message',\n PRIMARY KEY (`project`,`filename`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nPK\x07\x08\x0d\xa5T2x\x01\x00\x00x\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00 \x00new.shUT\x05\x00\x01\x80Cm8#!/bin/bash\ntouch $(date +%Y%m%d%H%M%S).up.sqlPK\x07\x08s\xd4N*.\x00\x00\x00.\x00\x00\x00PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xedzU\x8am \x00\x00m \x00\x00\x1a\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x00\x00\x00\x0020180704080000.base.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xf2\xc4\x87\xe8\xb5\x00\x00\x00\xb5\x00\x00\x00.\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xbe \x00\x0020181124181811.rename_and_prefix_tables.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(9\xa0\xdat8\x01\x00\x008\x01\x00\x00-\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xd8\n\x00\x0020181125100429.add_user_kind_and_owner.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x0d\xf9\xd3ga\x00\x00\x00a\x00\x00\x00-\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81t\x0c\x00\x0020181125153544.satosa_index_not_unique.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(f\x1f\x08\xd0\x9a\x03\x00\x00\x9a\x03\x00\x00!\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x819\x0d\x00\x0020181208140000.credentials.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(0V\x13\x0f4\x00\x00\x004\x00\x00\x00)\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81+\x11\x00\x0020190103203201.users-password-null.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x05\x10[\x91\x05\x01\x00\x00\x05\x01\x00\x00\x1b\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xbf\x11\x00\x0020190116102104.rules.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(s-\x98\xd0\x13\x01\x00\x00\x13\x01\x00\x00)\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x16\x13\x00\x0020190221001051.rename-team-to-role.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(z\",\xe8t\x0b\x00\x00t\x0b\x00\x00,\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x89\x14\x00\x0020190226160000.system_roles_and_rules.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xce\xaf\xb0\x07\x11\x07\x00\x00\x11\x07\x00\x00\"\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81` \x00\x0020190306205033.applications.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x0d\xa5T2x\x01\x00\x00x\x01\x00\x00\x0e\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xca'\x00\x00migrations.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(s\xd4N*.\x00\x00\x00.\x00\x00\x00\x06\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xed\x81\x87)\x00\x00new.shUT\x05\x00\x01\x80Cm8PK\x05\x06\x00\x00\x00\x00\x0c\x00\x0c\x00&\x04\x00\x00\xf2)\x00\x00\x00\x00" +var Asset = "PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1a\x00 \x0020180704080000.base.up.sqlUT\x05\x00\x01\x80Cm8-- all known organisations (crust instances) and our relation towards them\nCREATE TABLE organisations (\n id BIGINT UNSIGNED NOT NULL,\n fqn TEXT NOT NULL, -- fully qualified name of the organisation\n name TEXT NOT NULL, -- display name of the organisation\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n archived_at DATETIME NULL,\n deleted_at DATETIME NULL, -- organisation soft delete\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE settings (\n name VARCHAR(200) NOT NULL COMMENT 'Unique set of setting keys',\n value TEXT COMMENT 'Setting value',\n\n PRIMARY KEY (name)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\n-- Keeps all known users, home and external organisation\n-- changes are stored in audit log\nCREATE TABLE users (\n id BIGINT UNSIGNED NOT NULL,\n email TEXT NOT NULL,\n username TEXT NOT NULL,\n password TEXT NOT NULL,\n name TEXT NOT NULL,\n handle TEXT NOT NULL,\n meta JSON NOT NULL,\n satosa_id CHAR(36) NULL,\n\n rel_organisation BIGINT UNSIGNED NOT NULL,\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n suspended_at DATETIME NULL,\n deleted_at DATETIME NULL, -- user soft delete\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE UNIQUE INDEX uid_satosa ON users (satosa_id);\n\n-- Keeps all known teams\nCREATE TABLE teams (\n id BIGINT UNSIGNED NOT NULL,\n name TEXT NOT NULL, -- display name of the team\n handle TEXT NOT NULL, -- team handle string\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n archived_at DATETIME NULL,\n deleted_at DATETIME NULL, -- team soft delete\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\n-- Keeps team memberships\nCREATE TABLE team_members (\n rel_team BIGINT UNSIGNED NOT NULL REFERENCES organisation(id),\n rel_user BIGINT UNSIGNED NOT NULL,\n\n PRIMARY KEY (rel_team, rel_user)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08\xedzU\x8am \x00\x00m \x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00.\x00 \x0020181124181811.rename_and_prefix_tables.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE teams RENAME TO sys_team;\nALTER TABLE organisations RENAME TO sys_organisation;\nALTER TABLE team_members RENAME TO sys_team_member;\nALTER TABLE users RENAME TO sys_user;PK\x07\x08\xf2\xc4\x87\xe8\xb5\x00\x00\x00\xb5\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00-\x00 \x0020181125100429.add_user_kind_and_owner.up.sqlUT\x05\x00\x01\x80Cm8# add field to manage user type (bot support)\nALTER TABLE `sys_user` ADD `kind` VARCHAR(8) NOT NULL DEFAULT '' AFTER `handle`;\n\n# add field to manage \"ownership\" (get all bots created by user)\nALTER TABLE `sys_user` ADD `rel_user_id` BIGINT UNSIGNED NOT NULL AFTER `rel_organisation`, ADD INDEX (`rel_user_id`);\nPK\x07\x089\xa0\xdat8\x01\x00\x008\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00-\x00 \x0020181125153544.satosa_index_not_unique.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `sys_user` DROP INDEX `uid_satosa`, ADD INDEX `uid_satosa` (`satosa_id`) USING BTREE;PK\x07\x08\x0d\xf9\xd3ga\x00\x00\x00a\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00 \x0020181208140000.credentials.up.sqlUT\x05\x00\x01\x80Cm8-- Keeps all known users, home and external organisation\n-- changes are stored in audit log\nCREATE TABLE sys_credentials (\n id BIGINT UNSIGNED NOT NULL,\n rel_owner BIGINT UNSIGNED NOT NULL REFERENCES sys_users(id),\n label TEXT NOT NULL COMMENT 'something we can differentiate credentials by',\n kind VARCHAR(128) NOT NULL COMMENT 'hash, facebook, gplus, github, linkedin ...',\n credentials TEXT NOT NULL COMMENT 'crypted/hashed passwords, secrets, social profile ID',\n meta JSON NOT NULL,\n expires_at DATETIME NULL,\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n deleted_at DATETIME NULL, -- user soft delete\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE INDEX idx_owner ON sys_credentials (rel_owner);\nPK\x07\x08f\x1f\x08\xd0\x9a\x03\x00\x00\x9a\x03\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00)\x00 \x0020190103203201.users-password-null.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `sys_user` MODIFY `password` TEXT NULL;\nPK\x07\x080V\x13\x0f4\x00\x00\x004\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1b\x00 \x0020190116102104.rules.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE `sys_rules` (\n `rel_team` BIGINT UNSIGNED NOT NULL,\n `resource` VARCHAR(128) NOT NULL,\n `operation` VARCHAR(128) NOT NULL,\n `value` TINYINT(1) NOT NULL,\n\n PRIMARY KEY (`rel_team`, `resource`, `operation`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08\x05\x10[\x91\x05\x01\x00\x00\x05\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00)\x00 \x0020190221001051.rename-team-to-role.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE sys_team RENAME TO sys_role;\nALTER TABLE sys_team_member RENAME TO sys_role_member;\n\nALTER TABLE `sys_role_member` CHANGE COLUMN `rel_team` `rel_role` BIGINT UNSIGNED NOT NULL;\nALTER TABLE `sys_rules` CHANGE COLUMN `rel_team` `rel_role` BIGINT UNSIGNED NOT NULL;\nPK\x07\x08s-\x98\xd0\x13\x01\x00\x00\x13\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00,\x00 \x0020190226160000.system_roles_and_rules.up.sqlUT\x05\x00\x01\x80Cm8REPLACE INTO `sys_role` (`id`, `name`, `handle`) VALUES\n (1, 'Everyone', 'everyone'),\n (2, 'Administrators', 'admins');\n\n-- Value: Allow (2), Deny (1), Inherit(0),\nREPLACE INTO `sys_rules` (`rel_role`, `resource`, `operation`, `value`) VALUES\n-- Everyone\n (1, 'compose:*', 'access', 2),\n (1, 'messaging:*', 'access', 2),\n-- Admins\n (2, 'compose', 'namespace.create', 2),\n (2, 'compose', 'access', 2),\n (2, 'compose', 'grant', 2),\n (2, 'compose:namespace:*', 'page.create', 2),\n (2, 'compose:namespace:*', 'read', 2),\n (2, 'compose:namespace:*', 'update', 2),\n (2, 'compose:namespace:*', 'delete', 2),\n (2, 'compose:namespace:*', 'module.create', 2),\n (2, 'compose:namespace:*', 'chart.create', 2),\n (2, 'compose:namespace:*', 'trigger.create', 2),\n (2, 'compose:chart:*', 'read', 2),\n (2, 'compose:chart:*', 'update', 2),\n (2, 'compose:chart:*', 'delete', 2),\n (2, 'compose:trigger:*', 'read', 2),\n (2, 'compose:trigger:*', 'update', 2),\n (2, 'compose:trigger:*', 'delete', 2),\n (2, 'compose:page:*', 'read', 2),\n (2, 'compose:page:*', 'update', 2),\n (2, 'compose:page:*', 'delete', 2),\n (2, 'system', 'access', 2),\n (2, 'system', 'grant', 2),\n (2, 'system', 'organisation.create', 2),\n (2, 'system', 'role.create', 2),\n (2, 'system:organisation:*', 'access', 2),\n (2, 'system:role:*', 'read', 2),\n (2, 'system:role:*', 'update', 2),\n (2, 'system:role:*', 'delete', 2),\n (2, 'system:role:*', 'members.manage', 2),\n (2, 'messaging', 'access', 2),\n (2, 'messaging', 'grant', 2),\n (2, 'messaging', 'channel.public.create', 2),\n (2, 'messaging', 'channel.private.create', 2),\n (2, 'messaging', 'channel.group.create', 2),\n (2, 'messaging:channel:*', 'update', 2),\n (2, 'messaging:channel:*', 'leave', 2),\n (2, 'messaging:channel:*', 'read', 2),\n (2, 'messaging:channel:*', 'join', 2),\n (2, 'messaging:channel:*', 'delete', 2),\n (2, 'messaging:channel:*', 'undelete', 2),\n (2, 'messaging:channel:*', 'archive', 2),\n (2, 'messaging:channel:*', 'unarchive', 2),\n (2, 'messaging:channel:*', 'members.manage', 2),\n (2, 'messaging:channel:*', 'webhooks.manage', 2),\n (2, 'messaging:channel:*', 'attachments.manage', 2),\n (2, 'messaging:channel:*', 'message.attach', 2),\n (2, 'messaging:channel:*', 'message.update.all', 2),\n (2, 'messaging:channel:*', 'message.update.own', 2),\n (2, 'messaging:channel:*', 'message.delete.all', 2),\n (2, 'messaging:channel:*', 'message.delete.own', 2),\n (2, 'messaging:channel:*', 'message.embed', 2),\n (2, 'messaging:channel:*', 'message.send', 2),\n (2, 'messaging:channel:*', 'message.reply', 2),\n (2, 'messaging:channel:*', 'message.react', 2),\n (2, 'compose:module:*', 'read', 2),\n (2, 'compose:module:*', 'update', 2),\n (2, 'compose:module:*', 'delete', 2),\n (2, 'compose:module:*', 'record.create', 2),\n (2, 'compose:module:*', 'record.read', 2),\n (2, 'compose:module:*', 'record.update', 2),\n (2, 'compose:module:*', 'record.delete', 2);\nPK\x07\x08z\",\xe8t\x0b\x00\x00t\x0b\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\"\x00 \x0020190306205033.applications.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE sys_application (\n id BIGINT UNSIGNED NOT NULL,\n rel_owner BIGINT UNSIGNED NOT NULL REFERENCES sys_users(id),\n name TEXT NOT NULL COMMENT 'something we can differentiate application by',\n enabled BOOL NOT NULL,\n\n unify JSON NULL COMMENT 'unify specific settings',\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n deleted_at DATETIME NULL, -- user soft delete\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\n\nREPLACE INTO `sys_application` (`id`, `name`, `enabled`, `rel_owner`, `unify`) VALUES\n( 1, 'Crust Messaging', true, 0,\n '{\"logo\": \"/applications/crust.jpg\", \"icon\": \"/applications/crust_favicon.png\", \"url\": \"/messaging/\", \"listed\": true}'\n),\n( 2, 'Crust CRM', true, 0,\n '{\"logo\": \"/applications/crust.jpg\", \"icon\": \"/applications/crust_favicon.png\", \"url\": \"/crm/\", \"listed\": true}'\n),\n( 3, 'Crust Admin Area', true, 0,\n '{\"logo\": \"/applications/crust.jpg\", \"icon\": \"/applications/crust_favicon.png\", \"url\": \"/admin/\", \"listed\": true}'\n),\n( 4, 'Corteza Jitsi Bridge', true, 0,\n '{\"logo\": \"/applications/jitsi.png\", \"icon\": \"/applications/jitsi_icon.png\", \"url\": \"/bridge/jitsi/\", \"listed\": true}'\n),\n( 5, 'Google Maps', true, 0,\n '{\"logo\": \"/applications/google_maps.png\", \"icon\": \"/applications/google_maps_icon.png\", \"url\": \"/bridge/google-maps/\", \"listed\": true}'\n);\n\n-- Allow admin access to applications\nINSERT INTO `sys_rules` (`rel_role`, `resource`, `operation`, `value`) VALUES\n (1, 'system:application:*', 'read', 2),\n (2, 'system', 'application.create', 2),\n (2, 'system:application:*', 'read', 2),\n (2, 'system:application:*', 'update', 2),\n (2, 'system:application:*', 'delete', 2)\n;\nPK\x07\x08\xce\xaf\xb0\x07\x11\x07\x00\x00\x11\x07\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1e\x00 \x0020190326122000.settings.up.sqlUT\x05\x00\x01\x80Cm8DROP TABLE `settings`;\n\nCREATE TABLE `sys_settings` (\n rel_owner BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Value owner, 0 for global settings',\n name VARCHAR(200) NOT NULL COMMENT 'Unique set of setting keys',\n value JSON COMMENT 'Setting value',\n\n updated_at DATETIME NOT NULL DEFAULT NOW() COMMENT 'When was the value updated',\n updated_by BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Who created/updated the value',\n\n PRIMARY KEY (name, rel_owner)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08\xba\xa4\xab\xaf\\\x02\x00\x00\\\x02\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00#\x00 \x0020190403113201.users-cleanup.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `sys_user` DROP `password`;\nALTER TABLE `sys_user` DROP `satosa_id`;\nALTER TABLE `sys_credentials` ADD `last_used_at` DATETIME NULL;\nPK\x07\x088\x92\x0fs\x91\x00\x00\x00\x91\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00 \x00migrations.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE IF NOT EXISTS `migrations` (\n `project` varchar(16) NOT NULL COMMENT 'sam, crm, ...',\n `filename` varchar(255) NOT NULL COMMENT 'yyyymmddHHMMSS.sql',\n `statement_index` int(11) NOT NULL COMMENT 'Statement number from SQL file',\n `status` TEXT NOT NULL COMMENT 'ok or full error message',\n PRIMARY KEY (`project`,`filename`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nPK\x07\x08\x0d\xa5T2x\x01\x00\x00x\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00 \x00new.shUT\x05\x00\x01\x80Cm8#!/bin/bash\ntouch $(date +%Y%m%d%H%M%S).up.sqlPK\x07\x08s\xd4N*.\x00\x00\x00.\x00\x00\x00PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xedzU\x8am \x00\x00m \x00\x00\x1a\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x00\x00\x00\x0020180704080000.base.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xf2\xc4\x87\xe8\xb5\x00\x00\x00\xb5\x00\x00\x00.\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xbe \x00\x0020181124181811.rename_and_prefix_tables.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(9\xa0\xdat8\x01\x00\x008\x01\x00\x00-\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xd8\n\x00\x0020181125100429.add_user_kind_and_owner.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x0d\xf9\xd3ga\x00\x00\x00a\x00\x00\x00-\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81t\x0c\x00\x0020181125153544.satosa_index_not_unique.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(f\x1f\x08\xd0\x9a\x03\x00\x00\x9a\x03\x00\x00!\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x819\x0d\x00\x0020181208140000.credentials.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(0V\x13\x0f4\x00\x00\x004\x00\x00\x00)\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81+\x11\x00\x0020190103203201.users-password-null.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x05\x10[\x91\x05\x01\x00\x00\x05\x01\x00\x00\x1b\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xbf\x11\x00\x0020190116102104.rules.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(s-\x98\xd0\x13\x01\x00\x00\x13\x01\x00\x00)\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x16\x13\x00\x0020190221001051.rename-team-to-role.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(z\",\xe8t\x0b\x00\x00t\x0b\x00\x00,\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x89\x14\x00\x0020190226160000.system_roles_and_rules.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xce\xaf\xb0\x07\x11\x07\x00\x00\x11\x07\x00\x00\"\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81` \x00\x0020190306205033.applications.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xba\xa4\xab\xaf\\\x02\x00\x00\\\x02\x00\x00\x1e\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xca'\x00\x0020190326122000.settings.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(8\x92\x0fs\x91\x00\x00\x00\x91\x00\x00\x00#\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81{*\x00\x0020190403113201.users-cleanup.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x0d\xa5T2x\x01\x00\x00x\x01\x00\x00\x0e\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81f+\x00\x00migrations.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(s\xd4N*.\x00\x00\x00.\x00\x00\x00\x06\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xed\x81#-\x00\x00new.shUT\x05\x00\x01\x80Cm8PK\x05\x06\x00\x00\x00\x00\x0e\x00\x0e\x00\xd5\x04\x00\x00\x8e-\x00\x00\x00\x00" diff --git a/system/db/mysql/statik.go b/system/db/mysql/statik.go index 858bfca8c..b3f603baf 100644 --- a/system/db/mysql/statik.go +++ b/system/db/mysql/statik.go @@ -8,7 +8,7 @@ import ( ) func Data() string { - return "PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1a\x00 \x0020180704080000.base.up.sqlUT\x05\x00\x01\x80Cm8-- all known organisations (crust instances) and our relation towards them\nCREATE TABLE organisations (\n id BIGINT UNSIGNED NOT NULL,\n fqn TEXT NOT NULL, -- fully qualified name of the organisation\n name TEXT NOT NULL, -- display name of the organisation\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n archived_at DATETIME NULL,\n deleted_at DATETIME NULL, -- organisation soft delete\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE settings (\n name VARCHAR(200) NOT NULL COMMENT 'Unique set of setting keys',\n value TEXT COMMENT 'Setting value',\n\n PRIMARY KEY (name)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\n-- Keeps all known users, home and external organisation\n-- changes are stored in audit log\nCREATE TABLE users (\n id BIGINT UNSIGNED NOT NULL,\n email TEXT NOT NULL,\n username TEXT NOT NULL,\n password TEXT NOT NULL,\n name TEXT NOT NULL,\n handle TEXT NOT NULL,\n meta JSON NOT NULL,\n satosa_id CHAR(36) NULL,\n\n rel_organisation BIGINT UNSIGNED NOT NULL,\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n suspended_at DATETIME NULL,\n deleted_at DATETIME NULL, -- user soft delete\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE UNIQUE INDEX uid_satosa ON users (satosa_id);\n\n-- Keeps all known teams\nCREATE TABLE teams (\n id BIGINT UNSIGNED NOT NULL,\n name TEXT NOT NULL, -- display name of the team\n handle TEXT NOT NULL, -- team handle string\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n archived_at DATETIME NULL,\n deleted_at DATETIME NULL, -- team soft delete\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\n-- Keeps team memberships\nCREATE TABLE team_members (\n rel_team BIGINT UNSIGNED NOT NULL REFERENCES organisation(id),\n rel_user BIGINT UNSIGNED NOT NULL,\n\n PRIMARY KEY (rel_team, rel_user)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08\xedzU\x8am \x00\x00m \x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00.\x00 \x0020181124181811.rename_and_prefix_tables.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE teams RENAME TO sys_team;\nALTER TABLE organisations RENAME TO sys_organisation;\nALTER TABLE team_members RENAME TO sys_team_member;\nALTER TABLE users RENAME TO sys_user;PK\x07\x08\xf2\xc4\x87\xe8\xb5\x00\x00\x00\xb5\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00-\x00 \x0020181125100429.add_user_kind_and_owner.up.sqlUT\x05\x00\x01\x80Cm8# add field to manage user type (bot support)\nALTER TABLE `sys_user` ADD `kind` VARCHAR(8) NOT NULL DEFAULT '' AFTER `handle`;\n\n# add field to manage \"ownership\" (get all bots created by user)\nALTER TABLE `sys_user` ADD `rel_user_id` BIGINT UNSIGNED NOT NULL AFTER `rel_organisation`, ADD INDEX (`rel_user_id`);\nPK\x07\x089\xa0\xdat8\x01\x00\x008\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00-\x00 \x0020181125153544.satosa_index_not_unique.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `sys_user` DROP INDEX `uid_satosa`, ADD INDEX `uid_satosa` (`satosa_id`) USING BTREE;PK\x07\x08\x0d\xf9\xd3ga\x00\x00\x00a\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00 \x0020181208140000.credentials.up.sqlUT\x05\x00\x01\x80Cm8-- Keeps all known users, home and external organisation\n-- changes are stored in audit log\nCREATE TABLE sys_credentials (\n id BIGINT UNSIGNED NOT NULL,\n rel_owner BIGINT UNSIGNED NOT NULL REFERENCES sys_users(id),\n label TEXT NOT NULL COMMENT 'something we can differentiate credentials by',\n kind VARCHAR(128) NOT NULL COMMENT 'hash, facebook, gplus, github, linkedin ...',\n credentials TEXT NOT NULL COMMENT 'crypted/hashed passwords, secrets, social profile ID',\n meta JSON NOT NULL,\n expires_at DATETIME NULL,\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n deleted_at DATETIME NULL, -- user soft delete\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE INDEX idx_owner ON sys_credentials (rel_owner);\nPK\x07\x08f\x1f\x08\xd0\x9a\x03\x00\x00\x9a\x03\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00)\x00 \x0020190103203201.users-password-null.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `sys_user` MODIFY `password` TEXT NULL;\nPK\x07\x080V\x13\x0f4\x00\x00\x004\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1b\x00 \x0020190116102104.rules.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE `sys_rules` (\n `rel_team` BIGINT UNSIGNED NOT NULL,\n `resource` VARCHAR(128) NOT NULL,\n `operation` VARCHAR(128) NOT NULL,\n `value` TINYINT(1) NOT NULL,\n\n PRIMARY KEY (`rel_team`, `resource`, `operation`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08\x05\x10[\x91\x05\x01\x00\x00\x05\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00)\x00 \x0020190221001051.rename-team-to-role.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE sys_team RENAME TO sys_role;\nALTER TABLE sys_team_member RENAME TO sys_role_member;\n\nALTER TABLE `sys_role_member` CHANGE COLUMN `rel_team` `rel_role` BIGINT UNSIGNED NOT NULL;\nALTER TABLE `sys_rules` CHANGE COLUMN `rel_team` `rel_role` BIGINT UNSIGNED NOT NULL;\nPK\x07\x08s-\x98\xd0\x13\x01\x00\x00\x13\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00,\x00 \x0020190226160000.system_roles_and_rules.up.sqlUT\x05\x00\x01\x80Cm8REPLACE INTO `sys_role` (`id`, `name`, `handle`) VALUES\n (1, 'Everyone', 'everyone'),\n (2, 'Administrators', 'admins');\n\n-- Value: Allow (2), Deny (1), Inherit(0),\nREPLACE INTO `sys_rules` (`rel_role`, `resource`, `operation`, `value`) VALUES\n-- Everyone\n (1, 'compose:*', 'access', 2),\n (1, 'messaging:*', 'access', 2),\n-- Admins\n (2, 'compose', 'namespace.create', 2),\n (2, 'compose', 'access', 2),\n (2, 'compose', 'grant', 2),\n (2, 'compose:namespace:*', 'page.create', 2),\n (2, 'compose:namespace:*', 'read', 2),\n (2, 'compose:namespace:*', 'update', 2),\n (2, 'compose:namespace:*', 'delete', 2),\n (2, 'compose:namespace:*', 'module.create', 2),\n (2, 'compose:namespace:*', 'chart.create', 2),\n (2, 'compose:namespace:*', 'trigger.create', 2),\n (2, 'compose:chart:*', 'read', 2),\n (2, 'compose:chart:*', 'update', 2),\n (2, 'compose:chart:*', 'delete', 2),\n (2, 'compose:trigger:*', 'read', 2),\n (2, 'compose:trigger:*', 'update', 2),\n (2, 'compose:trigger:*', 'delete', 2),\n (2, 'compose:page:*', 'read', 2),\n (2, 'compose:page:*', 'update', 2),\n (2, 'compose:page:*', 'delete', 2),\n (2, 'system', 'access', 2),\n (2, 'system', 'grant', 2),\n (2, 'system', 'organisation.create', 2),\n (2, 'system', 'role.create', 2),\n (2, 'system:organisation:*', 'access', 2),\n (2, 'system:role:*', 'read', 2),\n (2, 'system:role:*', 'update', 2),\n (2, 'system:role:*', 'delete', 2),\n (2, 'system:role:*', 'members.manage', 2),\n (2, 'messaging', 'access', 2),\n (2, 'messaging', 'grant', 2),\n (2, 'messaging', 'channel.public.create', 2),\n (2, 'messaging', 'channel.private.create', 2),\n (2, 'messaging', 'channel.group.create', 2),\n (2, 'messaging:channel:*', 'update', 2),\n (2, 'messaging:channel:*', 'leave', 2),\n (2, 'messaging:channel:*', 'read', 2),\n (2, 'messaging:channel:*', 'join', 2),\n (2, 'messaging:channel:*', 'delete', 2),\n (2, 'messaging:channel:*', 'undelete', 2),\n (2, 'messaging:channel:*', 'archive', 2),\n (2, 'messaging:channel:*', 'unarchive', 2),\n (2, 'messaging:channel:*', 'members.manage', 2),\n (2, 'messaging:channel:*', 'webhooks.manage', 2),\n (2, 'messaging:channel:*', 'attachments.manage', 2),\n (2, 'messaging:channel:*', 'message.attach', 2),\n (2, 'messaging:channel:*', 'message.update.all', 2),\n (2, 'messaging:channel:*', 'message.update.own', 2),\n (2, 'messaging:channel:*', 'message.delete.all', 2),\n (2, 'messaging:channel:*', 'message.delete.own', 2),\n (2, 'messaging:channel:*', 'message.embed', 2),\n (2, 'messaging:channel:*', 'message.send', 2),\n (2, 'messaging:channel:*', 'message.reply', 2),\n (2, 'messaging:channel:*', 'message.react', 2),\n (2, 'compose:module:*', 'read', 2),\n (2, 'compose:module:*', 'update', 2),\n (2, 'compose:module:*', 'delete', 2),\n (2, 'compose:module:*', 'record.create', 2),\n (2, 'compose:module:*', 'record.read', 2),\n (2, 'compose:module:*', 'record.update', 2),\n (2, 'compose:module:*', 'record.delete', 2);\nPK\x07\x08z\",\xe8t\x0b\x00\x00t\x0b\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\"\x00 \x0020190306205033.applications.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE sys_application (\n id BIGINT UNSIGNED NOT NULL,\n rel_owner BIGINT UNSIGNED NOT NULL REFERENCES sys_users(id),\n name TEXT NOT NULL COMMENT 'something we can differentiate application by',\n enabled BOOL NOT NULL,\n\n unify JSON NULL COMMENT 'unify specific settings',\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n deleted_at DATETIME NULL, -- user soft delete\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\n\nREPLACE INTO `sys_application` (`id`, `name`, `enabled`, `rel_owner`, `unify`) VALUES\n( 1, 'Crust Messaging', true, 0,\n '{\"logo\": \"/applications/crust.jpg\", \"icon\": \"/applications/crust_favicon.png\", \"url\": \"/messaging/\", \"listed\": true}'\n),\n( 2, 'Crust CRM', true, 0,\n '{\"logo\": \"/applications/crust.jpg\", \"icon\": \"/applications/crust_favicon.png\", \"url\": \"/crm/\", \"listed\": true}'\n),\n( 3, 'Crust Admin Area', true, 0,\n '{\"logo\": \"/applications/crust.jpg\", \"icon\": \"/applications/crust_favicon.png\", \"url\": \"/admin/\", \"listed\": true}'\n),\n( 4, 'Corteza Jitsi Bridge', true, 0,\n '{\"logo\": \"/applications/jitsi.png\", \"icon\": \"/applications/jitsi_icon.png\", \"url\": \"/bridge/jitsi/\", \"listed\": true}'\n),\n( 5, 'Google Maps', true, 0,\n '{\"logo\": \"/applications/google_maps.png\", \"icon\": \"/applications/google_maps_icon.png\", \"url\": \"/bridge/google-maps/\", \"listed\": true}'\n);\n\n-- Allow admin access to applications\nINSERT INTO `sys_rules` (`rel_role`, `resource`, `operation`, `value`) VALUES\n (1, 'system:application:*', 'read', 2),\n (2, 'system', 'application.create', 2),\n (2, 'system:application:*', 'read', 2),\n (2, 'system:application:*', 'update', 2),\n (2, 'system:application:*', 'delete', 2)\n;\nPK\x07\x08\xce\xaf\xb0\x07\x11\x07\x00\x00\x11\x07\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1e\x00 \x0020190326122000.settings.up.sqlUT\x05\x00\x01\x80Cm8DROP TABLE `settings`;\n\nCREATE TABLE `sys_settings` (\n rel_owner BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Value owner, 0 for global settings',\n name VARCHAR(200) NOT NULL COMMENT 'Unique set of setting keys',\n value JSON COMMENT 'Setting value',\n\n updated_at DATETIME NOT NULL DEFAULT NOW() COMMENT 'When was the value updated',\n updated_by BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Who created/updated the value',\n\n PRIMARY KEY (name, rel_owner)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08\xba\xa4\xab\xaf\\\x02\x00\x00\\\x02\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00 \x00migrations.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE IF NOT EXISTS `migrations` (\n `project` varchar(16) NOT NULL COMMENT 'sam, crm, ...',\n `filename` varchar(255) NOT NULL COMMENT 'yyyymmddHHMMSS.sql',\n `statement_index` int(11) NOT NULL COMMENT 'Statement number from SQL file',\n `status` TEXT NOT NULL COMMENT 'ok or full error message',\n PRIMARY KEY (`project`,`filename`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nPK\x07\x08\x0d\xa5T2x\x01\x00\x00x\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00 \x00new.shUT\x05\x00\x01\x80Cm8#!/bin/bash\ntouch $(date +%Y%m%d%H%M%S).up.sqlPK\x07\x08s\xd4N*.\x00\x00\x00.\x00\x00\x00PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xedzU\x8am \x00\x00m \x00\x00\x1a\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x00\x00\x00\x0020180704080000.base.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xf2\xc4\x87\xe8\xb5\x00\x00\x00\xb5\x00\x00\x00.\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xbe \x00\x0020181124181811.rename_and_prefix_tables.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(9\xa0\xdat8\x01\x00\x008\x01\x00\x00-\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xd8\n\x00\x0020181125100429.add_user_kind_and_owner.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x0d\xf9\xd3ga\x00\x00\x00a\x00\x00\x00-\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81t\x0c\x00\x0020181125153544.satosa_index_not_unique.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(f\x1f\x08\xd0\x9a\x03\x00\x00\x9a\x03\x00\x00!\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x819\x0d\x00\x0020181208140000.credentials.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(0V\x13\x0f4\x00\x00\x004\x00\x00\x00)\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81+\x11\x00\x0020190103203201.users-password-null.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x05\x10[\x91\x05\x01\x00\x00\x05\x01\x00\x00\x1b\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xbf\x11\x00\x0020190116102104.rules.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(s-\x98\xd0\x13\x01\x00\x00\x13\x01\x00\x00)\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x16\x13\x00\x0020190221001051.rename-team-to-role.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(z\",\xe8t\x0b\x00\x00t\x0b\x00\x00,\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x89\x14\x00\x0020190226160000.system_roles_and_rules.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xce\xaf\xb0\x07\x11\x07\x00\x00\x11\x07\x00\x00\"\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81` \x00\x0020190306205033.applications.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xba\xa4\xab\xaf\\\x02\x00\x00\\\x02\x00\x00\x1e\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xca'\x00\x0020190326122000.settings.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x0d\xa5T2x\x01\x00\x00x\x01\x00\x00\x0e\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81{*\x00\x00migrations.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(s\xd4N*.\x00\x00\x00.\x00\x00\x00\x06\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xed\x818,\x00\x00new.shUT\x05\x00\x01\x80Cm8PK\x05\x06\x00\x00\x00\x00\x0d\x00\x0d\x00{\x04\x00\x00\xa3,\x00\x00\x00\x00" + return "PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1a\x00 \x0020180704080000.base.up.sqlUT\x05\x00\x01\x80Cm8-- all known organisations (crust instances) and our relation towards them\nCREATE TABLE organisations (\n id BIGINT UNSIGNED NOT NULL,\n fqn TEXT NOT NULL, -- fully qualified name of the organisation\n name TEXT NOT NULL, -- display name of the organisation\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n archived_at DATETIME NULL,\n deleted_at DATETIME NULL, -- organisation soft delete\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE settings (\n name VARCHAR(200) NOT NULL COMMENT 'Unique set of setting keys',\n value TEXT COMMENT 'Setting value',\n\n PRIMARY KEY (name)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\n-- Keeps all known users, home and external organisation\n-- changes are stored in audit log\nCREATE TABLE users (\n id BIGINT UNSIGNED NOT NULL,\n email TEXT NOT NULL,\n username TEXT NOT NULL,\n password TEXT NOT NULL,\n name TEXT NOT NULL,\n handle TEXT NOT NULL,\n meta JSON NOT NULL,\n satosa_id CHAR(36) NULL,\n\n rel_organisation BIGINT UNSIGNED NOT NULL,\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n suspended_at DATETIME NULL,\n deleted_at DATETIME NULL, -- user soft delete\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE UNIQUE INDEX uid_satosa ON users (satosa_id);\n\n-- Keeps all known teams\nCREATE TABLE teams (\n id BIGINT UNSIGNED NOT NULL,\n name TEXT NOT NULL, -- display name of the team\n handle TEXT NOT NULL, -- team handle string\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n archived_at DATETIME NULL,\n deleted_at DATETIME NULL, -- team soft delete\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\n-- Keeps team memberships\nCREATE TABLE team_members (\n rel_team BIGINT UNSIGNED NOT NULL REFERENCES organisation(id),\n rel_user BIGINT UNSIGNED NOT NULL,\n\n PRIMARY KEY (rel_team, rel_user)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08\xedzU\x8am \x00\x00m \x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00.\x00 \x0020181124181811.rename_and_prefix_tables.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE teams RENAME TO sys_team;\nALTER TABLE organisations RENAME TO sys_organisation;\nALTER TABLE team_members RENAME TO sys_team_member;\nALTER TABLE users RENAME TO sys_user;PK\x07\x08\xf2\xc4\x87\xe8\xb5\x00\x00\x00\xb5\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00-\x00 \x0020181125100429.add_user_kind_and_owner.up.sqlUT\x05\x00\x01\x80Cm8# add field to manage user type (bot support)\nALTER TABLE `sys_user` ADD `kind` VARCHAR(8) NOT NULL DEFAULT '' AFTER `handle`;\n\n# add field to manage \"ownership\" (get all bots created by user)\nALTER TABLE `sys_user` ADD `rel_user_id` BIGINT UNSIGNED NOT NULL AFTER `rel_organisation`, ADD INDEX (`rel_user_id`);\nPK\x07\x089\xa0\xdat8\x01\x00\x008\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00-\x00 \x0020181125153544.satosa_index_not_unique.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `sys_user` DROP INDEX `uid_satosa`, ADD INDEX `uid_satosa` (`satosa_id`) USING BTREE;PK\x07\x08\x0d\xf9\xd3ga\x00\x00\x00a\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00 \x0020181208140000.credentials.up.sqlUT\x05\x00\x01\x80Cm8-- Keeps all known users, home and external organisation\n-- changes are stored in audit log\nCREATE TABLE sys_credentials (\n id BIGINT UNSIGNED NOT NULL,\n rel_owner BIGINT UNSIGNED NOT NULL REFERENCES sys_users(id),\n label TEXT NOT NULL COMMENT 'something we can differentiate credentials by',\n kind VARCHAR(128) NOT NULL COMMENT 'hash, facebook, gplus, github, linkedin ...',\n credentials TEXT NOT NULL COMMENT 'crypted/hashed passwords, secrets, social profile ID',\n meta JSON NOT NULL,\n expires_at DATETIME NULL,\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n deleted_at DATETIME NULL, -- user soft delete\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE INDEX idx_owner ON sys_credentials (rel_owner);\nPK\x07\x08f\x1f\x08\xd0\x9a\x03\x00\x00\x9a\x03\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00)\x00 \x0020190103203201.users-password-null.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `sys_user` MODIFY `password` TEXT NULL;\nPK\x07\x080V\x13\x0f4\x00\x00\x004\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1b\x00 \x0020190116102104.rules.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE `sys_rules` (\n `rel_team` BIGINT UNSIGNED NOT NULL,\n `resource` VARCHAR(128) NOT NULL,\n `operation` VARCHAR(128) NOT NULL,\n `value` TINYINT(1) NOT NULL,\n\n PRIMARY KEY (`rel_team`, `resource`, `operation`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08\x05\x10[\x91\x05\x01\x00\x00\x05\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00)\x00 \x0020190221001051.rename-team-to-role.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE sys_team RENAME TO sys_role;\nALTER TABLE sys_team_member RENAME TO sys_role_member;\n\nALTER TABLE `sys_role_member` CHANGE COLUMN `rel_team` `rel_role` BIGINT UNSIGNED NOT NULL;\nALTER TABLE `sys_rules` CHANGE COLUMN `rel_team` `rel_role` BIGINT UNSIGNED NOT NULL;\nPK\x07\x08s-\x98\xd0\x13\x01\x00\x00\x13\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00,\x00 \x0020190226160000.system_roles_and_rules.up.sqlUT\x05\x00\x01\x80Cm8REPLACE INTO `sys_role` (`id`, `name`, `handle`) VALUES\n (1, 'Everyone', 'everyone'),\n (2, 'Administrators', 'admins');\n\n-- Value: Allow (2), Deny (1), Inherit(0),\nREPLACE INTO `sys_rules` (`rel_role`, `resource`, `operation`, `value`) VALUES\n-- Everyone\n (1, 'compose:*', 'access', 2),\n (1, 'messaging:*', 'access', 2),\n-- Admins\n (2, 'compose', 'namespace.create', 2),\n (2, 'compose', 'access', 2),\n (2, 'compose', 'grant', 2),\n (2, 'compose:namespace:*', 'page.create', 2),\n (2, 'compose:namespace:*', 'read', 2),\n (2, 'compose:namespace:*', 'update', 2),\n (2, 'compose:namespace:*', 'delete', 2),\n (2, 'compose:namespace:*', 'module.create', 2),\n (2, 'compose:namespace:*', 'chart.create', 2),\n (2, 'compose:namespace:*', 'trigger.create', 2),\n (2, 'compose:chart:*', 'read', 2),\n (2, 'compose:chart:*', 'update', 2),\n (2, 'compose:chart:*', 'delete', 2),\n (2, 'compose:trigger:*', 'read', 2),\n (2, 'compose:trigger:*', 'update', 2),\n (2, 'compose:trigger:*', 'delete', 2),\n (2, 'compose:page:*', 'read', 2),\n (2, 'compose:page:*', 'update', 2),\n (2, 'compose:page:*', 'delete', 2),\n (2, 'system', 'access', 2),\n (2, 'system', 'grant', 2),\n (2, 'system', 'organisation.create', 2),\n (2, 'system', 'role.create', 2),\n (2, 'system:organisation:*', 'access', 2),\n (2, 'system:role:*', 'read', 2),\n (2, 'system:role:*', 'update', 2),\n (2, 'system:role:*', 'delete', 2),\n (2, 'system:role:*', 'members.manage', 2),\n (2, 'messaging', 'access', 2),\n (2, 'messaging', 'grant', 2),\n (2, 'messaging', 'channel.public.create', 2),\n (2, 'messaging', 'channel.private.create', 2),\n (2, 'messaging', 'channel.group.create', 2),\n (2, 'messaging:channel:*', 'update', 2),\n (2, 'messaging:channel:*', 'leave', 2),\n (2, 'messaging:channel:*', 'read', 2),\n (2, 'messaging:channel:*', 'join', 2),\n (2, 'messaging:channel:*', 'delete', 2),\n (2, 'messaging:channel:*', 'undelete', 2),\n (2, 'messaging:channel:*', 'archive', 2),\n (2, 'messaging:channel:*', 'unarchive', 2),\n (2, 'messaging:channel:*', 'members.manage', 2),\n (2, 'messaging:channel:*', 'webhooks.manage', 2),\n (2, 'messaging:channel:*', 'attachments.manage', 2),\n (2, 'messaging:channel:*', 'message.attach', 2),\n (2, 'messaging:channel:*', 'message.update.all', 2),\n (2, 'messaging:channel:*', 'message.update.own', 2),\n (2, 'messaging:channel:*', 'message.delete.all', 2),\n (2, 'messaging:channel:*', 'message.delete.own', 2),\n (2, 'messaging:channel:*', 'message.embed', 2),\n (2, 'messaging:channel:*', 'message.send', 2),\n (2, 'messaging:channel:*', 'message.reply', 2),\n (2, 'messaging:channel:*', 'message.react', 2),\n (2, 'compose:module:*', 'read', 2),\n (2, 'compose:module:*', 'update', 2),\n (2, 'compose:module:*', 'delete', 2),\n (2, 'compose:module:*', 'record.create', 2),\n (2, 'compose:module:*', 'record.read', 2),\n (2, 'compose:module:*', 'record.update', 2),\n (2, 'compose:module:*', 'record.delete', 2);\nPK\x07\x08z\",\xe8t\x0b\x00\x00t\x0b\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\"\x00 \x0020190306205033.applications.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE sys_application (\n id BIGINT UNSIGNED NOT NULL,\n rel_owner BIGINT UNSIGNED NOT NULL REFERENCES sys_users(id),\n name TEXT NOT NULL COMMENT 'something we can differentiate application by',\n enabled BOOL NOT NULL,\n\n unify JSON NULL COMMENT 'unify specific settings',\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n deleted_at DATETIME NULL, -- user soft delete\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\n\nREPLACE INTO `sys_application` (`id`, `name`, `enabled`, `rel_owner`, `unify`) VALUES\n( 1, 'Crust Messaging', true, 0,\n '{\"logo\": \"/applications/crust.jpg\", \"icon\": \"/applications/crust_favicon.png\", \"url\": \"/messaging/\", \"listed\": true}'\n),\n( 2, 'Crust CRM', true, 0,\n '{\"logo\": \"/applications/crust.jpg\", \"icon\": \"/applications/crust_favicon.png\", \"url\": \"/crm/\", \"listed\": true}'\n),\n( 3, 'Crust Admin Area', true, 0,\n '{\"logo\": \"/applications/crust.jpg\", \"icon\": \"/applications/crust_favicon.png\", \"url\": \"/admin/\", \"listed\": true}'\n),\n( 4, 'Corteza Jitsi Bridge', true, 0,\n '{\"logo\": \"/applications/jitsi.png\", \"icon\": \"/applications/jitsi_icon.png\", \"url\": \"/bridge/jitsi/\", \"listed\": true}'\n),\n( 5, 'Google Maps', true, 0,\n '{\"logo\": \"/applications/google_maps.png\", \"icon\": \"/applications/google_maps_icon.png\", \"url\": \"/bridge/google-maps/\", \"listed\": true}'\n);\n\n-- Allow admin access to applications\nINSERT INTO `sys_rules` (`rel_role`, `resource`, `operation`, `value`) VALUES\n (1, 'system:application:*', 'read', 2),\n (2, 'system', 'application.create', 2),\n (2, 'system:application:*', 'read', 2),\n (2, 'system:application:*', 'update', 2),\n (2, 'system:application:*', 'delete', 2)\n;\nPK\x07\x08\xce\xaf\xb0\x07\x11\x07\x00\x00\x11\x07\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1e\x00 \x0020190326122000.settings.up.sqlUT\x05\x00\x01\x80Cm8DROP TABLE `settings`;\n\nCREATE TABLE `sys_settings` (\n rel_owner BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Value owner, 0 for global settings',\n name VARCHAR(200) NOT NULL COMMENT 'Unique set of setting keys',\n value JSON COMMENT 'Setting value',\n\n updated_at DATETIME NOT NULL DEFAULT NOW() COMMENT 'When was the value updated',\n updated_by BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Who created/updated the value',\n\n PRIMARY KEY (name, rel_owner)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08\xba\xa4\xab\xaf\\\x02\x00\x00\\\x02\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00#\x00 \x0020190403113201.users-cleanup.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `sys_user` DROP `password`;\nALTER TABLE `sys_user` DROP `satosa_id`;\nALTER TABLE `sys_credentials` ADD `last_used_at` DATETIME NULL;\nPK\x07\x088\x92\x0fs\x91\x00\x00\x00\x91\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00 \x00migrations.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE IF NOT EXISTS `migrations` (\n `project` varchar(16) NOT NULL COMMENT 'sam, crm, ...',\n `filename` varchar(255) NOT NULL COMMENT 'yyyymmddHHMMSS.sql',\n `statement_index` int(11) NOT NULL COMMENT 'Statement number from SQL file',\n `status` TEXT NOT NULL COMMENT 'ok or full error message',\n PRIMARY KEY (`project`,`filename`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nPK\x07\x08\x0d\xa5T2x\x01\x00\x00x\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00 \x00new.shUT\x05\x00\x01\x80Cm8#!/bin/bash\ntouch $(date +%Y%m%d%H%M%S).up.sqlPK\x07\x08s\xd4N*.\x00\x00\x00.\x00\x00\x00PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xedzU\x8am \x00\x00m \x00\x00\x1a\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x00\x00\x00\x0020180704080000.base.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xf2\xc4\x87\xe8\xb5\x00\x00\x00\xb5\x00\x00\x00.\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xbe \x00\x0020181124181811.rename_and_prefix_tables.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(9\xa0\xdat8\x01\x00\x008\x01\x00\x00-\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xd8\n\x00\x0020181125100429.add_user_kind_and_owner.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x0d\xf9\xd3ga\x00\x00\x00a\x00\x00\x00-\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81t\x0c\x00\x0020181125153544.satosa_index_not_unique.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(f\x1f\x08\xd0\x9a\x03\x00\x00\x9a\x03\x00\x00!\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x819\x0d\x00\x0020181208140000.credentials.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(0V\x13\x0f4\x00\x00\x004\x00\x00\x00)\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81+\x11\x00\x0020190103203201.users-password-null.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x05\x10[\x91\x05\x01\x00\x00\x05\x01\x00\x00\x1b\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xbf\x11\x00\x0020190116102104.rules.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(s-\x98\xd0\x13\x01\x00\x00\x13\x01\x00\x00)\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x16\x13\x00\x0020190221001051.rename-team-to-role.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(z\",\xe8t\x0b\x00\x00t\x0b\x00\x00,\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x89\x14\x00\x0020190226160000.system_roles_and_rules.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xce\xaf\xb0\x07\x11\x07\x00\x00\x11\x07\x00\x00\"\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81` \x00\x0020190306205033.applications.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xba\xa4\xab\xaf\\\x02\x00\x00\\\x02\x00\x00\x1e\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xca'\x00\x0020190326122000.settings.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(8\x92\x0fs\x91\x00\x00\x00\x91\x00\x00\x00#\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81{*\x00\x0020190403113201.users-cleanup.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x0d\xa5T2x\x01\x00\x00x\x01\x00\x00\x0e\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81f+\x00\x00migrations.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(s\xd4N*.\x00\x00\x00.\x00\x00\x00\x06\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xed\x81#-\x00\x00new.shUT\x05\x00\x01\x80Cm8PK\x05\x06\x00\x00\x00\x00\x0e\x00\x0e\x00\xd5\x04\x00\x00\x8e-\x00\x00\x00\x00" } func init() { diff --git a/system/db/schema/mysql/20190403113201.users-cleanup.up.sql b/system/db/schema/mysql/20190403113201.users-cleanup.up.sql new file mode 100644 index 000000000..b02326c51 --- /dev/null +++ b/system/db/schema/mysql/20190403113201.users-cleanup.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE `sys_user` DROP `password`; +ALTER TABLE `sys_user` DROP `satosa_id`; +ALTER TABLE `sys_credentials` ADD `last_used_at` DATETIME NULL; diff --git a/system/flags.go b/system/flags.go index 57298982c..97196f15f 100644 --- a/system/flags.go +++ b/system/flags.go @@ -12,8 +12,6 @@ type ( http *config.HTTP monitor *config.Monitor db *config.Database - oidc *config.OIDC - social *config.Social } ) @@ -35,12 +33,6 @@ func (c *appFlags) Validate() error { if err := c.db.Validate(); err != nil { return err } - if err := c.oidc.Validate(); err != nil { - return err - } - if err := c.social.Validate(); err != nil { - return err - } return nil } @@ -56,7 +48,5 @@ func Flags(prefix ...string) { new(config.HTTP).Init(prefix...), new(config.Monitor).Init(prefix...), new(config.Database).Init(prefix...), - new(config.OIDC).Init(prefix...), - new(config.Social).Init(prefix...), } } diff --git a/system/internal/auth/external/external.go b/system/internal/auth/external/external.go new file mode 100644 index 000000000..ad8186a07 --- /dev/null +++ b/system/internal/auth/external/external.go @@ -0,0 +1,15 @@ +package external + +import ( + "log" + + "github.com/crusttech/crust/internal/settings" +) + +func Init(settingsService settings.Service) { + if eas, err := ExternalAuthSettings(settingsService); err != nil { + log.Printf("failed load external authentication settings: %v", err) + } else { + setupGoth(eas) + } +} diff --git a/system/internal/auth/external/goth.go b/system/internal/auth/external/goth.go new file mode 100644 index 000000000..056db8d2b --- /dev/null +++ b/system/internal/auth/external/goth.go @@ -0,0 +1,100 @@ +package external + +import ( + "log" + "strings" + + "github.com/gorilla/sessions" + "github.com/markbates/goth" + "github.com/markbates/goth/gothic" + "github.com/markbates/goth/providers/facebook" + "github.com/markbates/goth/providers/github" + "github.com/markbates/goth/providers/gplus" + "github.com/markbates/goth/providers/linkedin" + "github.com/markbates/goth/providers/openidConnect" +) + +// We're expecting that our users will be able to complete +// external auth loop in 15 minutes. +const ( + gothMaxSessionStoreAge = 60 * 15 // In seconds. +) + +func setupGoth(eas *externalAuthSettings) { + if eas == nil || !eas.enabled { + log.Printf("external authentication disabled (%v)", eas) + return + } + + store := sessions.NewCookieStore([]byte(eas.sessionStoreSecret)) + store.MaxAge(gothMaxSessionStoreAge) + store.Options.Path = "/auth/external" + store.Options.HttpOnly = true + store.Options.Secure = eas.sessionStoreSecure + gothic.Store = store + + setupGothProviders(eas) + +} + +func setupGothProviders(eas *externalAuthSettings) { + var ( + err error + scopes = []string{"email"} + ) + + // Purge all previously configured providers + if l := len(goth.GetProviders()); l > 0 { + log.Println("removing existing providers", l) + goth.ClearProviders() + } + + var enabled uint + for _, pc := range eas.providers { + if pc.enabled { + enabled++ + } + } + + log.Printf("initializing external authentication providers (%d)", enabled) + + for name, pc := range eas.providers { + var provider goth.Provider + + if !pc.enabled { + continue + } + + if strings.Index(name, "openid-connect.") == 0 { + if pc.issuerUrl == "" { + log.Printf("failed to discover %q (issuer URL is empty)", name) + continue + } + + wellKnown := strings.TrimSuffix(pc.issuerUrl, "/") + "/.well-known/openid-configuration" + + if provider, err = openidConnect.New(pc.key, pc.secret, pc.redirectUrl, wellKnown, scopes...); err != nil { + log.Printf("failed to discover %q (auto discovery URL: %s) OIDC provider: %v", name, wellKnown, err) + continue + } else { + provider.SetName(name) + } + } else { + switch name { + case "github": + provider = github.New(pc.key, pc.secret, pc.redirectUrl, scopes...) + case "facebook": + provider = facebook.New(pc.key, pc.secret, pc.redirectUrl, scopes...) + case "gplus": + provider = gplus.New(pc.key, pc.secret, pc.redirectUrl, scopes...) + case "linkedin": + provider = linkedin.New(pc.key, pc.secret, pc.redirectUrl, scopes...) + } + } + + if provider != nil { + log.Printf("external authentication provider %q added", name) + goth.UseProviders(provider) + } + } +} diff --git a/system/internal/auth/external/register.go b/system/internal/auth/external/register.go new file mode 100644 index 000000000..79623d28a --- /dev/null +++ b/system/internal/auth/external/register.go @@ -0,0 +1,39 @@ +package external + +import ( + "context" + "fmt" + + "github.com/crusttech/go-oidc" +) + +func RegisterNewOpenIdClient(ctx context.Context, eas *externalAuthSettings, name, url string) (eap *externalAuthProvider, err error) { + var ( + provider *oidc.Provider + client *oidc.Client + redirectUrl = fmt.Sprintf(eas.redirectUrl, "openid-connect."+name) + ) + + if provider, err = oidc.NewProvider(ctx, url); err != nil { + return + } + + client, err = provider.RegisterClient(ctx, &oidc.ClientRegistration{ + Name: "Crust", + RedirectURIs: []string{redirectUrl}, + ResponseTypes: []string{"token id_token", "code"}, + }) + + if err != nil { + return + } + + eap = &externalAuthProvider{ + redirectUrl: redirectUrl, + key: client.ID, + secret: client.Secret, + issuerUrl: url, + } + + return +} diff --git a/system/internal/auth/external/settings.go b/system/internal/auth/external/settings.go new file mode 100644 index 000000000..7a313becf --- /dev/null +++ b/system/internal/auth/external/settings.go @@ -0,0 +1,199 @@ +package external + +import ( + "encoding/json" + "fmt" + "os" + "strings" + + "github.com/pkg/errors" + + "github.com/crusttech/crust/internal/rand" + intset "github.com/crusttech/crust/internal/settings" +) + +const ( + settingsKeyBase = "auth.external." + + settingsKeyProviders = settingsKeyBase + "providers." + settingsKeyRedirectUrl = settingsKeyBase + "redirect-url" + settingsKeySessionStoreSecret = settingsKeyBase + "session-store-secret" + settingsKeySessionStoreSecure = settingsKeyBase + "session-store-secure" +) + +type ( + externalAuthSettings struct { + enabled bool + redirectUrl string + sessionStoreSecret string + sessionStoreSecure bool + providers map[string]externalAuthProvider + + service intset.Service + } + + externalAuthProvider struct { + enabled bool + key string + secret string + redirectUrl string + issuerUrl string + } +) + +func ExternalAuthProvider(kv intset.KV) (eap externalAuthProvider, err error) { + for k, v := range kv { + ld := strings.LastIndex(k, ".") + + switch k[ld+1:] { + case "enabled": + err = v.Unmarshal(&eap.enabled) + case "key": + err = v.Unmarshal(&eap.key) + case "secret": + err = v.Unmarshal(&eap.secret) + case "issuer": + err = v.Unmarshal(&eap.issuerUrl) + } + + if err != nil { + return + } + } + + return +} + +func (eap externalAuthProvider) MakeValueSet(name string) (vv intset.ValueSet, err error) { + set := func(name string, value interface{}) error { + v := &intset.Value{Name: name} + if v.Value, err = json.Marshal(value); err != nil { + return err + } + + vv = append(vv, v) + return nil + } + + prefix := settingsKeyProviders + name + + if err = set(prefix+".enabled", eap.enabled); err != nil { + return nil, err + } + + if err = set(prefix+".key", eap.key); err != nil { + return nil, err + } + + if err = set(prefix+".secret", eap.secret); err != nil { + return nil, err + } + + if err = set(prefix+".issuer", eap.issuerUrl); err != nil { + return nil, err + } + + return vv, err +} + +func ExternalAuthSettings(s intset.Service) (eas *externalAuthSettings, err error) { + // Read all settings and populate struct + settings, err := s.FindByPrefix(settingsKeyBase) + if err != nil { + return nil, errors.Wrap(err, "could not load settings for external auth provider") + } + + kv := settings.KV() + + eas = &externalAuthSettings{ + enabled: kv.Bool(settingsKeyBase + "enabled"), + redirectUrl: kv.String(settingsKeyRedirectUrl), + sessionStoreSecret: kv.String(settingsKeySessionStoreSecret), + } + + storeGenerated := func(name string, value interface{}) (err error) { + v := &intset.Value{Name: name} + if v.Value, err = json.Marshal(value); err != nil { + return + } + + return s.Set(v) + } + + if eas.redirectUrl == "" { + path := "/auth/external/%s/callback" + if leHost, has := os.LookupEnv("LETSENCRYPT_HOST"); has { + eas.redirectUrl = "https://" + leHost + path + } else if vHost, has := os.LookupEnv("VIRTUAL_HOST"); has { + eas.redirectUrl = "http://" + vHost + path + } else { + // Fallback to local + eas.redirectUrl = "http://system.api.local.crust.tech" + path + } + + if err = storeGenerated(settingsKeyRedirectUrl, eas.redirectUrl); err != nil { + return + } + } + + if eas.sessionStoreSecret == "" { + eas.sessionStoreSecret = string(rand.Bytes(64)) + if err = storeGenerated(settingsKeySessionStoreSecret, eas.sessionStoreSecret); err != nil { + return + } + } + + if !kv.Has(settingsKeySessionStoreSecure) { + // Assume we want to use secure store if we are accessed via HTTPS + eas.sessionStoreSecure = strings.Index(eas.redirectUrl, "https://") == 0 + } else { + eas.sessionStoreSecure = kv.Bool(settingsKeySessionStoreSecure) + } + + if eas.providers, err = extractProviders(eas.redirectUrl, kv); err != nil { + return nil, err + } + + return +} + +func extractProviders(redirectUrl string, kv intset.KV) (providers map[string]externalAuthProvider, err error) { + // Standard providers: + providers = map[string]externalAuthProvider{ + "github": {}, + "facebook": {}, + "gplus": {}, + "linkedin": {}, + } + + oidcKeyBase := settingsKeyProviders + "openid-connect." + for k := range kv.Filter(oidcKeyBase) { + if len(k) < len(oidcKeyBase)+2 { + // skip invalid keys + continue + } + + // find next dot: + name := k[len(oidcKeyBase):] + dotPos := strings.Index(name, ".") + if dotPos > 0 { + name = name[:dotPos] + } + + providers["openid-connect."+name] = externalAuthProvider{} + } + + for provider := range providers { + if eap, err := ExternalAuthProvider(kv.Filter(settingsKeyProviders + provider)); err != nil { + return nil, err + } else { + if eap.enabled { + eap.redirectUrl = fmt.Sprintf(redirectUrl, provider) + } + + providers[provider] = eap + } + } + + return +} diff --git a/system/internal/auth/external/settings_test.go b/system/internal/auth/external/settings_test.go new file mode 100644 index 000000000..3d7eb22bc --- /dev/null +++ b/system/internal/auth/external/settings_test.go @@ -0,0 +1,142 @@ +package external + +import ( + "reflect" + "testing" + + intset "github.com/crusttech/crust/internal/settings" + "github.com/jmoiron/sqlx/types" +) + +func Test_extractProviders(t *testing.T) { + type args struct { + redirectUrl string + kv intset.KV + } + tests := []struct { + name string + args args + wantProviders map[string]externalAuthProvider + wantErr bool + }{ + { + name: "Empty KV", + args: args{}, + wantProviders: map[string]externalAuthProvider{ + "github": externalAuthProvider{}, + "linkedin": externalAuthProvider{}, + "gplus": externalAuthProvider{}, + "facebook": externalAuthProvider{}, + }, + }, + + { + name: "Random config", + args: args{ + redirectUrl: "http://%s", + kv: intset.KV{ + "auth.external.providers.openid-connect.foo.enabled": types.JSONText("true"), + "auth.external.providers.openid-connect.foo.issuer": types.JSONText(`"url"`), + "auth.external.providers.openid-connect.foo.key": types.JSONText(`"key"`), + "auth.external.providers.openid-connect.foo.secret": types.JSONText(`"secret"`), + "auth.external.providers.openid-connect.bar.enabled": types.JSONText("true"), + "auth.external.providers.openid-connect.bar.issuer": types.JSONText(`"url"`), + "auth.external.providers.openid-connect.bar.key": types.JSONText(`"key"`), + "auth.external.providers.openid-connect.bar.secret": types.JSONText(`"secret"`), + "auth.external.providers.openid-connect.baz.enabled": types.JSONText("false"), + "auth.external.providers.github.enabled": types.JSONText(`false`), + "auth.external.providers.facebook.enabled": types.JSONText(`true`), + "auth.external.providers.facebook.secret": types.JSONText(`"fb-secret"`), + }, + }, + + wantProviders: map[string]externalAuthProvider{ + "openid-connect.foo": externalAuthProvider{ + enabled: true, + key: "key", + secret: "secret", + redirectUrl: "http://openid-connect.foo", + issuerUrl: "url", + }, + "openid-connect.bar": externalAuthProvider{ + enabled: true, + key: "key", + secret: "secret", + redirectUrl: "http://openid-connect.bar", + issuerUrl: "url", + }, + "openid-connect.baz": externalAuthProvider{ + enabled: false, + }, + "github": externalAuthProvider{ + enabled: false, + }, + "linkedin": externalAuthProvider{ + enabled: false, + }, + "gplus": externalAuthProvider{ + enabled: false, + }, + "facebook": externalAuthProvider{ + enabled: true, + secret: "fb-secret", + redirectUrl: "http://facebook", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotProviders, err := extractProviders(tt.args.redirectUrl, tt.args.kv) + if (err != nil) != tt.wantErr { + t.Errorf("extractProviders() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(gotProviders, tt.wantProviders) { + t.Errorf("extractProviders() = %v, want %v", gotProviders, tt.wantProviders) + } + }) + } +} + +func TestExternalAuthProvider(t *testing.T) { + type args struct { + kv intset.KV + } + tests := []struct { + name string + args args + wantEap externalAuthProvider + wantErr bool + }{ + { + args: args{ + kv: intset.KV{ + "foo.enabled": types.JSONText("true"), + "foo.issuer": types.JSONText(`"example.tld"`), + "foo.key": types.JSONText(`"key"`), + "foo.secret": types.JSONText(`"secret"`), + }, + }, + + wantEap: externalAuthProvider{ + enabled: true, + key: "key", + secret: "secret", + issuerUrl: "example.tld", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotEap, err := ExternalAuthProvider(tt.args.kv) + if (err != nil) != tt.wantErr { + t.Errorf("ExternalAuthProvider() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(gotEap, tt.wantEap) { + t.Errorf("ExternalAuthProvider() = %v, want %v", gotEap, tt.wantEap) + } + }) + } +} diff --git a/system/internal/repository/credentials.go b/system/internal/repository/credentials.go index 93f6d2245..adbe43fec 100644 --- a/system/internal/repository/credentials.go +++ b/system/internal/repository/credentials.go @@ -15,12 +15,13 @@ type ( With(ctx context.Context, db *factory.DB) CredentialsRepository FindByID(ID uint64) (*types.Credentials, error) - FindByCredentials(kind types.CredentialsKind, credentials string) (cc types.CredentialsSet, err error) - FindByKind(ownerID uint64, kind types.CredentialsKind) (cc types.CredentialsSet, err error) + FindByCredentials(kind, credentials string) (cc types.CredentialsSet, err error) + FindByKind(ownerID uint64, kind string) (cc types.CredentialsSet, err error) FindByOwnerID(ownerID uint64) (cc types.CredentialsSet, err error) Find() (cc types.CredentialsSet, err error) Create(c *types.Credentials) (*types.Credentials, error) + Update(c *types.Credentials) (*types.Credentials, error) DeleteByID(id uint64) error } @@ -59,14 +60,14 @@ func (r *credentials) FindByID(ID uint64) (*types.Credentials, error) { return mod, isFound(r.db().Get(mod, sql, ID), mod.ID > 0, ErrCredentialsNotFound) } -func (r *credentials) FindByCredentials(kind types.CredentialsKind, credentials string) (cc types.CredentialsSet, err error) { +func (r *credentials) FindByCredentials(kind, credentials string) (cc types.CredentialsSet, err error) { return r.fetchSet( fmt.Sprintf(sqlCredentialsSelect+" AND kind = ? AND credentials = ?", r.tblname), kind, credentials) } -func (r *credentials) FindByKind(ownerID uint64, kind types.CredentialsKind) (cc types.CredentialsSet, err error) { +func (r *credentials) FindByKind(ownerID uint64, kind string) (cc types.CredentialsSet, err error) { return r.fetchSet( fmt.Sprintf(sqlCredentialsSelect+" AND rel_owner = ? AND kind = ?", r.tblname), ownerID, @@ -95,6 +96,12 @@ func (r *credentials) Create(c *types.Credentials) (*types.Credentials, error) { return c, r.db().Insert(r.tblname, c) } +func (r *credentials) Update(c *types.Credentials) (*types.Credentials, error) { + updatedAt := time.Now() + c.UpdatedAt = &updatedAt + return c, r.db().Update(r.tblname, c) +} + func (r *credentials) DeleteByID(id uint64) error { return r.updateColumnByID(r.tblname, "deleted_at", time.Now(), id) } diff --git a/system/internal/repository/credentials_test.go b/system/internal/repository/credentials_test.go index 8e9d95d33..1e5203c71 100644 --- a/system/internal/repository/credentials_test.go +++ b/system/internal/repository/credentials_test.go @@ -29,9 +29,9 @@ func TestCredentials(t *testing.T) { db.Exec("DELETE FROM sys_credentials WHERE 1=1") cc := types.CredentialsSet{ - &types.Credentials{OwnerID: 10000, Kind: types.CredentialsKindLinkedin, Credentials: "linkedin-profile-id"}, - &types.Credentials{OwnerID: 10000, Kind: types.CredentialsKindGPlus, Credentials: "gplus-profile-id"}, - &types.Credentials{OwnerID: 20000, Kind: types.CredentialsKindFacebook, Credentials: "facebook-profile-id"}, + &types.Credentials{OwnerID: 10000, Kind: "li", Credentials: "linkedin-profile-id"}, + &types.Credentials{OwnerID: 10000, Kind: "g+", Credentials: "gplus-profile-id"}, + &types.Credentials{OwnerID: 20000, Kind: "fb", Credentials: "facebook-profile-id"}, } for _, c := range cc { diff --git a/system/internal/repository/mocks/credentials.go b/system/internal/repository/mocks/credentials.go index a6c0a48e9..f30979ece 100644 --- a/system/internal/repository/mocks/credentials.go +++ b/system/internal/repository/mocks/credentials.go @@ -38,7 +38,6 @@ func (m *MockCredentialsRepository) EXPECT() *MockCredentialsRepositoryMockRecor // With mocks base method func (m *MockCredentialsRepository) With(ctx context.Context, db *factory.DB) repository.CredentialsRepository { - m.ctrl.T.Helper() ret := m.ctrl.Call(m, "With", ctx, db) ret0, _ := ret[0].(repository.CredentialsRepository) return ret0 @@ -46,13 +45,11 @@ func (m *MockCredentialsRepository) With(ctx context.Context, db *factory.DB) re // With indicates an expected call of With func (mr *MockCredentialsRepositoryMockRecorder) With(ctx, db interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "With", reflect.TypeOf((*MockCredentialsRepository)(nil).With), ctx, db) } // FindByID mocks base method func (m *MockCredentialsRepository) FindByID(ID uint64) (*types.Credentials, error) { - m.ctrl.T.Helper() ret := m.ctrl.Call(m, "FindByID", ID) ret0, _ := ret[0].(*types.Credentials) ret1, _ := ret[1].(error) @@ -61,13 +58,11 @@ func (m *MockCredentialsRepository) FindByID(ID uint64) (*types.Credentials, err // FindByID indicates an expected call of FindByID func (mr *MockCredentialsRepositoryMockRecorder) FindByID(ID interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByID", reflect.TypeOf((*MockCredentialsRepository)(nil).FindByID), ID) } // FindByCredentials mocks base method -func (m *MockCredentialsRepository) FindByCredentials(kind types.CredentialsKind, credentials string) (types.CredentialsSet, error) { - m.ctrl.T.Helper() +func (m *MockCredentialsRepository) FindByCredentials(kind, credentials string) (types.CredentialsSet, error) { ret := m.ctrl.Call(m, "FindByCredentials", kind, credentials) ret0, _ := ret[0].(types.CredentialsSet) ret1, _ := ret[1].(error) @@ -76,13 +71,11 @@ func (m *MockCredentialsRepository) FindByCredentials(kind types.CredentialsKind // FindByCredentials indicates an expected call of FindByCredentials func (mr *MockCredentialsRepositoryMockRecorder) FindByCredentials(kind, credentials interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByCredentials", reflect.TypeOf((*MockCredentialsRepository)(nil).FindByCredentials), kind, credentials) } // FindByKind mocks base method -func (m *MockCredentialsRepository) FindByKind(ownerID uint64, kind types.CredentialsKind) (types.CredentialsSet, error) { - m.ctrl.T.Helper() +func (m *MockCredentialsRepository) FindByKind(ownerID uint64, kind string) (types.CredentialsSet, error) { ret := m.ctrl.Call(m, "FindByKind", ownerID, kind) ret0, _ := ret[0].(types.CredentialsSet) ret1, _ := ret[1].(error) @@ -91,13 +84,11 @@ func (m *MockCredentialsRepository) FindByKind(ownerID uint64, kind types.Creden // FindByKind indicates an expected call of FindByKind func (mr *MockCredentialsRepositoryMockRecorder) FindByKind(ownerID, kind interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByKind", reflect.TypeOf((*MockCredentialsRepository)(nil).FindByKind), ownerID, kind) } // FindByOwnerID mocks base method func (m *MockCredentialsRepository) FindByOwnerID(ownerID uint64) (types.CredentialsSet, error) { - m.ctrl.T.Helper() ret := m.ctrl.Call(m, "FindByOwnerID", ownerID) ret0, _ := ret[0].(types.CredentialsSet) ret1, _ := ret[1].(error) @@ -106,13 +97,11 @@ func (m *MockCredentialsRepository) FindByOwnerID(ownerID uint64) (types.Credent // FindByOwnerID indicates an expected call of FindByOwnerID func (mr *MockCredentialsRepositoryMockRecorder) FindByOwnerID(ownerID interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByOwnerID", reflect.TypeOf((*MockCredentialsRepository)(nil).FindByOwnerID), ownerID) } // Find mocks base method func (m *MockCredentialsRepository) Find() (types.CredentialsSet, error) { - m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Find") ret0, _ := ret[0].(types.CredentialsSet) ret1, _ := ret[1].(error) @@ -121,13 +110,11 @@ func (m *MockCredentialsRepository) Find() (types.CredentialsSet, error) { // Find indicates an expected call of Find func (mr *MockCredentialsRepositoryMockRecorder) Find() *gomock.Call { - mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Find", reflect.TypeOf((*MockCredentialsRepository)(nil).Find)) } // Create mocks base method func (m *MockCredentialsRepository) Create(c *types.Credentials) (*types.Credentials, error) { - m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Create", c) ret0, _ := ret[0].(*types.Credentials) ret1, _ := ret[1].(error) @@ -136,13 +123,24 @@ func (m *MockCredentialsRepository) Create(c *types.Credentials) (*types.Credent // Create indicates an expected call of Create func (mr *MockCredentialsRepositoryMockRecorder) Create(c interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockCredentialsRepository)(nil).Create), c) } +// Update mocks base method +func (m *MockCredentialsRepository) Update(c *types.Credentials) (*types.Credentials, error) { + ret := m.ctrl.Call(m, "Update", c) + ret0, _ := ret[0].(*types.Credentials) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Update indicates an expected call of Update +func (mr *MockCredentialsRepositoryMockRecorder) Update(c interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockCredentialsRepository)(nil).Update), c) +} + // DeleteByID mocks base method func (m *MockCredentialsRepository) DeleteByID(id uint64) error { - m.ctrl.T.Helper() ret := m.ctrl.Call(m, "DeleteByID", id) ret0, _ := ret[0].(error) return ret0 @@ -150,6 +148,5 @@ func (m *MockCredentialsRepository) DeleteByID(id uint64) error { // DeleteByID indicates an expected call of DeleteByID func (mr *MockCredentialsRepositoryMockRecorder) DeleteByID(id interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteByID", reflect.TypeOf((*MockCredentialsRepository)(nil).DeleteByID), id) } diff --git a/system/internal/repository/mocks/user.go b/system/internal/repository/mocks/user.go index f4644c18d..9a259a5c2 100644 --- a/system/internal/repository/mocks/user.go +++ b/system/internal/repository/mocks/user.go @@ -39,7 +39,6 @@ func (m *MockUserRepository) EXPECT() *MockUserRepositoryMockRecorder { // With mocks base method func (m *MockUserRepository) With(ctx context.Context, db *factory.DB) repository.UserRepository { - m.ctrl.T.Helper() ret := m.ctrl.Call(m, "With", ctx, db) ret0, _ := ret[0].(repository.UserRepository) return ret0 @@ -47,13 +46,11 @@ func (m *MockUserRepository) With(ctx context.Context, db *factory.DB) repositor // With indicates an expected call of With func (mr *MockUserRepositoryMockRecorder) With(ctx, db interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "With", reflect.TypeOf((*MockUserRepository)(nil).With), ctx, db) } // FindByEmail mocks base method func (m *MockUserRepository) FindByEmail(email string) (*types.User, error) { - m.ctrl.T.Helper() ret := m.ctrl.Call(m, "FindByEmail", email) ret0, _ := ret[0].(*types.User) ret1, _ := ret[1].(error) @@ -62,13 +59,11 @@ func (m *MockUserRepository) FindByEmail(email string) (*types.User, error) { // FindByEmail indicates an expected call of FindByEmail func (mr *MockUserRepositoryMockRecorder) FindByEmail(email interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByEmail", reflect.TypeOf((*MockUserRepository)(nil).FindByEmail), email) } // FindByUsername mocks base method func (m *MockUserRepository) FindByUsername(username string) (*types.User, error) { - m.ctrl.T.Helper() ret := m.ctrl.Call(m, "FindByUsername", username) ret0, _ := ret[0].(*types.User) ret1, _ := ret[1].(error) @@ -77,13 +72,11 @@ func (m *MockUserRepository) FindByUsername(username string) (*types.User, error // FindByUsername indicates an expected call of FindByUsername func (mr *MockUserRepositoryMockRecorder) FindByUsername(username interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByUsername", reflect.TypeOf((*MockUserRepository)(nil).FindByUsername), username) } // FindByID mocks base method func (m *MockUserRepository) FindByID(id uint64) (*types.User, error) { - m.ctrl.T.Helper() ret := m.ctrl.Call(m, "FindByID", id) ret0, _ := ret[0].(*types.User) ret1, _ := ret[1].(error) @@ -92,13 +85,11 @@ func (m *MockUserRepository) FindByID(id uint64) (*types.User, error) { // FindByID indicates an expected call of FindByID func (mr *MockUserRepositoryMockRecorder) FindByID(id interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByID", reflect.TypeOf((*MockUserRepository)(nil).FindByID), id) } // FindByIDs mocks base method func (m *MockUserRepository) FindByIDs(id ...uint64) (types.UserSet, error) { - m.ctrl.T.Helper() varargs := []interface{}{} for _, a := range id { varargs = append(varargs, a) @@ -111,28 +102,11 @@ func (m *MockUserRepository) FindByIDs(id ...uint64) (types.UserSet, error) { // FindByIDs indicates an expected call of FindByIDs func (mr *MockUserRepositoryMockRecorder) FindByIDs(id ...interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByIDs", reflect.TypeOf((*MockUserRepository)(nil).FindByIDs), id...) } -// FindBySatosaID mocks base method -func (m *MockUserRepository) FindBySatosaID(id string) (*types.User, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FindBySatosaID", id) - ret0, _ := ret[0].(*types.User) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// FindBySatosaID indicates an expected call of FindBySatosaID -func (mr *MockUserRepositoryMockRecorder) FindBySatosaID(id interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindBySatosaID", reflect.TypeOf((*MockUserRepository)(nil).FindBySatosaID), id) -} - // Find mocks base method func (m *MockUserRepository) Find(filter *types.UserFilter) ([]*types.User, error) { - m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Find", filter) ret0, _ := ret[0].([]*types.User) ret1, _ := ret[1].(error) @@ -141,13 +115,11 @@ func (m *MockUserRepository) Find(filter *types.UserFilter) ([]*types.User, erro // Find indicates an expected call of Find func (mr *MockUserRepositoryMockRecorder) Find(filter interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Find", reflect.TypeOf((*MockUserRepository)(nil).Find), filter) } // Create mocks base method func (m *MockUserRepository) Create(mod *types.User) (*types.User, error) { - m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Create", mod) ret0, _ := ret[0].(*types.User) ret1, _ := ret[1].(error) @@ -156,13 +128,11 @@ func (m *MockUserRepository) Create(mod *types.User) (*types.User, error) { // Create indicates an expected call of Create func (mr *MockUserRepositoryMockRecorder) Create(mod interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockUserRepository)(nil).Create), mod) } // Update mocks base method func (m *MockUserRepository) Update(mod *types.User) (*types.User, error) { - m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Update", mod) ret0, _ := ret[0].(*types.User) ret1, _ := ret[1].(error) @@ -171,13 +141,11 @@ func (m *MockUserRepository) Update(mod *types.User) (*types.User, error) { // Update indicates an expected call of Update func (mr *MockUserRepositoryMockRecorder) Update(mod interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockUserRepository)(nil).Update), mod) } // BindAvatar mocks base method func (m *MockUserRepository) BindAvatar(user *types.User, avatar io.Reader) (*types.User, error) { - m.ctrl.T.Helper() ret := m.ctrl.Call(m, "BindAvatar", user, avatar) ret0, _ := ret[0].(*types.User) ret1, _ := ret[1].(error) @@ -186,13 +154,11 @@ func (m *MockUserRepository) BindAvatar(user *types.User, avatar io.Reader) (*ty // BindAvatar indicates an expected call of BindAvatar func (mr *MockUserRepositoryMockRecorder) BindAvatar(user, avatar interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BindAvatar", reflect.TypeOf((*MockUserRepository)(nil).BindAvatar), user, avatar) } // SuspendByID mocks base method func (m *MockUserRepository) SuspendByID(id uint64) error { - m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SuspendByID", id) ret0, _ := ret[0].(error) return ret0 @@ -200,13 +166,11 @@ func (m *MockUserRepository) SuspendByID(id uint64) error { // SuspendByID indicates an expected call of SuspendByID func (mr *MockUserRepositoryMockRecorder) SuspendByID(id interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SuspendByID", reflect.TypeOf((*MockUserRepository)(nil).SuspendByID), id) } // UnsuspendByID mocks base method func (m *MockUserRepository) UnsuspendByID(id uint64) error { - m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UnsuspendByID", id) ret0, _ := ret[0].(error) return ret0 @@ -214,13 +178,11 @@ func (m *MockUserRepository) UnsuspendByID(id uint64) error { // UnsuspendByID indicates an expected call of UnsuspendByID func (mr *MockUserRepositoryMockRecorder) UnsuspendByID(id interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnsuspendByID", reflect.TypeOf((*MockUserRepository)(nil).UnsuspendByID), id) } // DeleteByID mocks base method func (m *MockUserRepository) DeleteByID(id uint64) error { - m.ctrl.T.Helper() ret := m.ctrl.Call(m, "DeleteByID", id) ret0, _ := ret[0].(error) return ret0 @@ -228,6 +190,5 @@ func (m *MockUserRepository) DeleteByID(id uint64) error { // DeleteByID indicates an expected call of DeleteByID func (mr *MockUserRepositoryMockRecorder) DeleteByID(id interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteByID", reflect.TypeOf((*MockUserRepository)(nil).DeleteByID), id) } diff --git a/system/internal/repository/role.go b/system/internal/repository/role.go index 7a3168052..c035eddf1 100644 --- a/system/internal/repository/role.go +++ b/system/internal/repository/role.go @@ -160,6 +160,12 @@ func (r *role) Reset() error { return err } + // Remove all rules for both roles + sql = "DELETE FROM sys_rules WHERE rel_role IN (1, 2)" + if _, err = r.db().Exec(sql); err != nil { + return err + } + // Value: Allow (2), Deny (1), Inherit(0) sql = `REPLACE INTO sys_rules (rel_role, resource, operation, value) VALUES -- Everyone diff --git a/system/internal/repository/user.go b/system/internal/repository/user.go index cc3608bd6..b603116a3 100644 --- a/system/internal/repository/user.go +++ b/system/internal/repository/user.go @@ -20,7 +20,6 @@ type ( FindByUsername(username string) (*types.User, error) FindByID(id uint64) (*types.User, error) FindByIDs(id ...uint64) (types.UserSet, error) - FindBySatosaID(id string) (*types.User, error) Find(filter *types.UserFilter) ([]*types.User, error) Create(mod *types.User) (*types.User, error) @@ -42,8 +41,8 @@ type ( ) const ( - sqlUserColumns = "id, email, username, password, name, handle, " + - "meta, satosa_id, rel_organisation, " + + sqlUserColumns = "id, email, username, name, handle, " + + "meta, rel_organisation, " + "created_at, updated_at, suspended_at, deleted_at" sqlUserScope = "suspended_at IS NULL AND deleted_at IS NULL" sqlUserSelect = "SELECT " + sqlUserColumns + " FROM %s WHERE " + sqlUserScope @@ -69,13 +68,6 @@ func (r *user) FindByUsername(username string) (*types.User, error) { return mod, isFound(r.db().Get(mod, sql, username), mod.ID > 0, ErrUserNotFound) } -func (r *user) FindBySatosaID(satosaID string) (*types.User, error) { - sql := fmt.Sprintf(sqlUserSelect, r.users) + " AND satosa_id = ?" - mod := &types.User{} - - return mod, isFound(r.db().Get(mod, sql, satosaID), mod.ID > 0, ErrUserNotFound) -} - func (r *user) FindByEmail(email string) (*types.User, error) { sql := fmt.Sprintf(sqlUserSelect, r.users) + " AND email = ?" mod := &types.User{} diff --git a/system/internal/repository/user_test.go b/system/internal/repository/user_test.go index cd699b67b..3ad655304 100644 --- a/system/internal/repository/user_test.go +++ b/system/internal/repository/user_test.go @@ -27,7 +27,6 @@ func TestUser(t *testing.T) { user := &types.User{ Name: "John User Doe", Username: "johndoe", - SatosaID: "1234", Meta: &types.UserMeta{ Avatar: "123", }, diff --git a/system/internal/service/auth.go b/system/internal/service/auth.go index e0e280e2a..87b314269 100644 --- a/system/internal/service/auth.go +++ b/system/internal/service/auth.go @@ -3,6 +3,7 @@ package service import ( "context" "log" + "time" "github.com/markbates/goth" "github.com/pkg/errors" @@ -23,7 +24,7 @@ type ( AuthService interface { With(ctx context.Context) AuthService - Social(profile goth.User) (*types.User, error) + External(profile goth.User) (*types.User, error) CheckPassword(username, password string) (*types.User, error) ChangePassword(user *types.User, password string) error @@ -50,14 +51,11 @@ func (svc *auth) With(ctx context.Context) AuthService { // Social user verifies existance by using email value from social profile and creates user if needed // // It does not update user's info -func (svc *auth) Social(profile goth.User) (u *types.User, err error) { - var kind types.CredentialsKind +func (svc *auth) External(profile goth.User) (u *types.User, err error) { + var lastUsedAt = time.Now() - switch profile.Provider { - case "facebook", "gplus", "github", "linkedin": - kind = types.CredentialsKind(profile.Provider) - default: - return nil, errors.New("Unsupported provider") + if _, err := goth.GetProvider(profile.Provider); err != nil { + return nil, err } if profile.Email == "" { @@ -66,7 +64,7 @@ func (svc *auth) Social(profile goth.User) (u *types.User, err error) { return u, svc.db.Transaction(func() error { var c *types.Credentials - if cc, err := svc.credentials.FindByCredentials(kind, profile.UserID); err == nil { + if cc, err := svc.credentials.FindByCredentials(profile.Provider, profile.UserID); err == nil { // Credentials found, load user for _, c := range cc { if !c.Valid() { @@ -110,6 +108,8 @@ func (svc *auth) Social(profile goth.User) (u *types.User, err error) { findByEmail: // Find user via his email if u, err = svc.users.FindByEmail(profile.Email); err == repository.ErrUserNotFound { + // @todo check if it is ok to auto-create a user here + // In case we do not have this email, create a new user u = &types.User{ Email: profile.Email, @@ -119,13 +119,35 @@ func (svc *auth) Social(profile goth.User) (u *types.User, err error) { } if u, err = svc.users.Create(u); err != nil { - return err + return errors.Wrap(err, "could not create user after successful social auth") } + log.Printf("Created new user after successful social auth (%v, %v)", u.ID, u.Email) + + // Owner created + return nil + } else if err != nil { + return err + } else if !u.Valid() { + return errors.Errorf( + "Social login to an invalid/suspended user (user ID: %v)", + u.ID, + ) + } else { + log.Printf( + "Autheticated user (%v, %v) via %s, existing user", + u.ID, + u.Email, + profile.Provider, + ) + } + + if c == nil { c = &types.Credentials{ - Kind: kind, + Kind: profile.Provider, OwnerID: u.ID, Credentials: profile.UserID, + LastUsedAt: &lastUsedAt, } if !profile.ExpiresAt.IsZero() { @@ -138,31 +160,32 @@ func (svc *auth) Social(profile goth.User) (u *types.User, err error) { } log.Printf( - "Autheticated user (%v, %v) via %s, created user and credentials (%v)", + "Creating new credential entry (%v, %v) for exisintg user (%v, %v)", + c.ID, + profile.Provider, u.ID, u.Email, - profile.Provider, - c.ID, ) + } else { + if !profile.ExpiresAt.IsZero() { + // Copy expiration date when provided + c.ExpiresAt = &profile.ExpiresAt + } - // Owner created - return nil - } else if err != nil { - return err - } else if !u.Valid() { - return errors.Errorf( - "Social login to an invalid/suspended user (user ID: %v)", + c.LastUsedAt = &lastUsedAt + if c, err = svc.credentials.Update(c); err != nil { + return err + } + + log.Printf( + "Updating credential entry (%v, %v) for exisintg user (%v, %v)", + c.ID, + profile.Provider, u.ID, + u.Email, ) } - log.Printf( - "Autheticated user (%v, %v) via %s, existing user", - u.ID, - u.Email, - profile.Provider, - ) - // Owner loaded, carry on. return nil }) diff --git a/system/internal/service/auth_test.go b/system/internal/service/auth_test.go index ea0dd8a48..26c9b07f3 100644 --- a/system/internal/service/auth_test.go +++ b/system/internal/service/auth_test.go @@ -24,7 +24,7 @@ func TestSocialSigninWithExistingCredentials(t *testing.T) { crdRpoMock := repomock.NewMockCredentialsRepository(mockCtrl) crdRpoMock.EXPECT(). - FindByCredentials(types.CredentialsKindGPlus, p.UserID). + FindByCredentials("g+", p.UserID). Times(1). Return(types.CredentialsSet{c}, nil) @@ -54,12 +54,12 @@ func TestSocialSigninWithNewUserCredentials(t *testing.T) { crdRpoMock := repomock.NewMockCredentialsRepository(mockCtrl) crdRpoMock.EXPECT(). - FindByCredentials(types.CredentialsKindGPlus, p.UserID). + FindByCredentials("g+", p.UserID). Times(1). Return(types.CredentialsSet{}, nil) crdRpoMock.EXPECT(). - Create(&types.Credentials{Kind: types.CredentialsKindGPlus, OwnerID: u.ID, Credentials: p.UserID}). + Create(&types.Credentials{Kind: "g+", OwnerID: u.ID, Credentials: p.UserID}). Times(1). Return(c, nil) diff --git a/system/internal/service/user.go b/system/internal/service/user.go index f9e971cf3..c1b8351b9 100644 --- a/system/internal/service/user.go +++ b/system/internal/service/user.go @@ -37,8 +37,6 @@ type ( FindByIDs(id ...uint64) (types.UserSet, error) Find(filter *types.UserFilter) (types.UserSet, error) - FindOrCreate(*types.User) (*types.User, error) - Create(input *types.User) (*types.User, error) Update(mod *types.User) (*types.User, error) @@ -85,36 +83,6 @@ func (svc *user) Find(filter *types.UserFilter) (types.UserSet, error) { return svc.user.Find(filter) } -// FindOrCreate finds existing or creates new user wrapped in db transaction -func (svc *user) FindOrCreate(user *types.User) (out *types.User, err error) { - return out, svc.db.Transaction(func() error { - if len(user.SatosaID) != uuidLength { - // @todo uuid format check - return errors.Errorf("Invalid UUID value (%v) for SATOSA ID", user.SatosaID) - } - - out, err = svc.user.FindBySatosaID(user.SatosaID) - - if err == repository.ErrUserNotFound { - // @todo do we allow autocreation of nonexisting users? - out, err = svc.user.Create(user) - return err - } - - if err != nil { - // FindBySatosaID error - return err - } - - out, err = svc.user.Update(out) - if err != nil { - return err - } - - return nil - }) -} - func (svc *user) Create(input *types.User) (out *types.User, err error) { return out, svc.db.Transaction(func() error { if !svc.prm.CanCreateUser() { @@ -124,6 +92,7 @@ func (svc *user) Create(input *types.User) (out *types.User, err error) { if out, err = svc.user.Create(input); err != nil { return err } + return nil }) } diff --git a/system/rest/auth.go b/system/rest/auth.go index fe295f76a..410d8fc90 100644 --- a/system/rest/auth.go +++ b/system/rest/auth.go @@ -79,7 +79,7 @@ func (ctrl *Auth) Handlers(jwtEncoder auth.TokenEncoder) *handlers.Auth { } h.Login = func(w http.ResponseWriter, r *http.Request) { params := request.NewAuthLogin() - ctx := r.Context() + // ctx := r.Context() // parse request to fill parameters err := params.Fill(r) @@ -88,26 +88,28 @@ func (ctrl *Auth) Handlers(jwtEncoder auth.TokenEncoder) *handlers.Auth { return } - userSvc := service.User(ctx) + // userSvc := service.User(ctx) // check email and username for login - user, err := userSvc.FindByEmail(params.Username) - if err != nil { - user, err = userSvc.FindByUsername(params.Username) - } - - // can't find user - if err != nil { - resputil.JSON(w, err) - return - } + // @todo disabled until we migrate to local users through credentials + // user, err := userSvc.FindByEmail(params.Username) + // if err != nil { + // user, err = userSvc.FindByUsername(params.Username) + // } + // + // // can't find user + // if err != nil { + // resputil.JSON(w, err) + // return + // } // validate password - if user.ValidatePassword(params.Password) { - jwtEncoder.SetCookie(w, r, user) - resputil.JSON(w, user, err) - return - } + // @todo disabled until we migrate to local users through credentials + // if user.ValidatePassword(params.Password) { + // jwtEncoder.SetCookie(w, r, user) + // resputil.JSON(w, user, err) + // return + // } resputil.JSON(w, errors.New("Password doesn't match")) } diff --git a/system/rest/auth_external.go b/system/rest/auth_external.go new file mode 100644 index 000000000..75b8b8f88 --- /dev/null +++ b/system/rest/auth_external.go @@ -0,0 +1,140 @@ +package rest + +import ( + "context" + "fmt" + "log" + "net/http" + "strings" + "time" + + "github.com/go-chi/chi" + "github.com/markbates/goth" + "github.com/markbates/goth/gothic" + "github.com/titpetric/factory/resputil" + + "github.com/crusttech/crust/internal/auth" + "github.com/crusttech/crust/system/internal/service" +) + +type ( + ExternalAuth struct { + auth service.AuthService + jwtEncoder auth.TokenEncoder + } +) + +const ( + externalAuthBaseUrl = "/auth/external" +) + +func NewSocial(jwtEncoder auth.TokenEncoder) *ExternalAuth { + return &ExternalAuth{ + auth: service.DefaultAuth, + jwtEncoder: jwtEncoder, + } +} + +func (ctrl *ExternalAuth) MountRoutes(r chi.Router) { + + // Make sure we're backwards compatible and redirect /oidc to /auth/external/openid-connect-didmos2 + r.Get("/oidc", func(w http.ResponseWriter, req *http.Request) { + http.Redirect(w, req, externalAuthBaseUrl+"/openid-connect-didmos2", http.StatusMovedPermanently) + }) + + // Copy provider from path (Chi URL param) to request context and return it + copyProviderToContext := func(r *http.Request) *http.Request { + return r.WithContext(context.WithValue(r.Context(), "provider", chi.URLParam(r, "provider"))) + } + + r.Route(externalAuthBaseUrl+"/{provider}", func(r chi.Router) { + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + r = copyProviderToContext(r) + + // Always set redir cookie, even if not requested. If param is empty, cookie is removed + ctrl.setSessionCookie(w, r, "redir", r.URL.Query().Get("redir")) + + // try to get the user without re-authenticating + if user, err := gothic.CompleteUserAuth(w, r); err != nil { + gothic.BeginAuthHandler(w, r) + } else { + // We've successfully singed-in through 3rd party auth + ctrl.handleSuccessfulAuth(w, r, user) + } + }) + + r.Get("/callback", func(w http.ResponseWriter, r *http.Request) { + r = copyProviderToContext(r) + + if user, err := gothic.CompleteUserAuth(w, r); err != nil { + log.Printf("Failed to complete user auth: %v", err) + ctrl.handleFailedCallback(w, r, err) + } else { + ctrl.handleSuccessfulAuth(w, r, user) + } + }) + + r.Get("/logout", func(w http.ResponseWriter, r *http.Request) { + if err := gothic.Logout(w, r); err != nil { + log.Printf("Failed to external logout: %v", err) + } + + w.Header().Set("Location", "/") + w.WriteHeader(http.StatusTemporaryRedirect) + }) + }) +} + +func (ctrl *ExternalAuth) handleFailedCallback(w http.ResponseWriter, r *http.Request, err error) { + provider := chi.URLParam(r, "provider") + + if strings.Contains(err.Error(), "Error processing your OAuth request: Invalid oauth_verifier parameter") { + // Just take user through the same loop again + w.Header().Set("Location", externalAuthBaseUrl+"/"+provider) + w.WriteHeader(http.StatusSeeOther) + return + } + + fmt.Fprintf(w, "SSO Error: %v", err.Error()) + w.WriteHeader(http.StatusOK) +} + +// Handles authentication via external auth providers of +// unknown an user + appending authentication on external providers +// to a current user +func (ctrl *ExternalAuth) handleSuccessfulAuth(w http.ResponseWriter, r *http.Request, cred goth.User) { + log.Printf("Successful external login: %v", cred) + + if u, err := ctrl.auth.With(r.Context()).External(cred); err != nil { + resputil.JSON(w, err) + } else { + ctrl.jwtEncoder.SetCookie(w, r, u) + + if c, err := r.Cookie("redir"); c != nil && err == nil { + ctrl.setSessionCookie(w, r, "redir", "") + w.Header().Set("Location", c.Value) + w.WriteHeader(http.StatusSeeOther) + + } + + resputil.JSON(w, u, err) + } +} + +// Extracts and authenticates JWT from context, validates claims +func (ctrl *ExternalAuth) setSessionCookie(w http.ResponseWriter, r *http.Request, name, value string) { + cookie := &http.Cookie{ + Name: name, + Secure: r.URL.Scheme == "https", + Domain: r.URL.Hostname(), + Path: externalAuthBaseUrl, + } + + if value == "" { + cookie.Expires = time.Unix(0, 0) + } else { + cookie.Value = value + } + + http.SetCookie(w, cookie) +} diff --git a/system/rest/oidc.go b/system/rest/oidc.go deleted file mode 100644 index 0d615c2aa..000000000 --- a/system/rest/oidc.go +++ /dev/null @@ -1,221 +0,0 @@ -package rest - -import ( - "context" - "log" - "math/rand" - "net/http" - "strconv" - "time" - - "github.com/pkg/errors" - "github.com/titpetric/factory/resputil" - "golang.org/x/oauth2" - - "github.com/crusttech/go-oidc" - - "github.com/crusttech/crust/internal/auth" - "github.com/crusttech/crust/internal/config" - "github.com/crusttech/crust/system/internal/repository" - "github.com/crusttech/crust/system/internal/service" - "github.com/crusttech/crust/system/types" -) - -const DB_SETTINGS_KEY_OIDC_CLIENT = "oidc-client" - -type ( - openIdConnect struct { - provider *oidc.Provider - verifier *oidc.IDTokenVerifier - config oauth2.Config - - appURL string - stateCookieExpiry int64 - stateCookieDomain string - - userService service.UserService - - jwt auth.TokenEncoder - } - - oidcProfile struct { - Email string `json:"email"` - Name string `json:"name"` - Sub string `json:"sub"` - } -) - -const openIdConnectStateCookie = "oidc-state" - -// Sets-up OIDC connection (issuer discovery, client registration) -// -// Client registration is done when no cfg.ClientID is provided. -func OpenIdConnect(ctx context.Context, cfg *config.OIDC, usvc service.UserService, jwt auth.TokenEncoder, settings repository.Settings) (c *openIdConnect, err error) { - c = &openIdConnect{ - appURL: cfg.AppURL, - stateCookieExpiry: cfg.StateCookieExpiry, - stateCookieDomain: cfg.StateCookieDomain, - userService: usvc, - jwt: jwt, - } - - // Allow 5 seconds for issuer discovery process - c.provider, err = oidc.NewProvider(ctx, cfg.Issuer) - if err != nil { - return nil, err - } - - if len(cfg.ClientID) > 0 { - // System is configured with fixed OIDC client ID (probably through AUTH_OIDC_CLIENT_ID) - // Construct oauth2 config from provided configuration - c.config = oauth2.Config{ - ClientID: cfg.ClientID, - ClientSecret: cfg.ClientSecret, - RedirectURL: cfg.RedirectURL, - - // Discovery returns the OAuth2 endpoints. - Endpoint: c.provider.Endpoint(), - } - } else { - client := &oidc.Client{} - - if found, err := settings.Get(DB_SETTINGS_KEY_OIDC_CLIENT, client); err != nil { - return nil, errors.Wrap(err, "could not load oidc client settings from the database") - } else if !found || client == nil { - log.Printf("OIDC client not found, registering new") - - // Perform dynamic client registration - client, err = c.provider.RegisterClient(ctx, &oidc.ClientRegistration{ - Name: "Crust", - RedirectURIs: []string{cfg.RedirectURL}, - ResponseTypes: []string{"token id_token", "code"}, - }) - - if err != nil { - return nil, errors.Wrap(err, "could not register OIDC client") - } - - if err := settings.Set(DB_SETTINGS_KEY_OIDC_CLIENT, client); err != nil { - return nil, errors.Wrap(err, "could not store oidc client settings from the database") - } - } - - c.config = c.provider.OAuth2Config(client) - } - - c.config.Scopes = []string{ - oidc.ScopeOpenID, - "email", - "profile", - "address", - "phone_number", - } - - c.verifier = c.provider.Verifier(&oidc.Config{ClientID: c.config.ClientID}) - - return -} - -// Redirects user to the issuer's login screen -func (c *openIdConnect) HandleRedirect(w http.ResponseWriter, r *http.Request) { - // @todo sure we can improve this... - rand.Seed(4321) - var state = strconv.FormatInt(rand.Int63(), 10) - - // Store state to cookie as well - c.setStateCookie(w, r, state) - - http.Redirect(w, r, c.config.AuthCodeURL(state), http.StatusFound) -} - -// Handles callback from issuer -// -// If everything goes well (scope & token verification) it reads issued claims, -// creates Crust JWT and stores it in a cookie. -// -// @todo All failed responses must redirect to appURL as well + some error code that will be displayed on the client -func (c *openIdConnect) HandleOAuth2Callback(w http.ResponseWriter, r *http.Request) { - var ctx = r.Context() - - if !c.stateCheck(r) { - resputil.JSON(w, "State check failed") - return - } - - c.setStateCookie(w, r, "") // remove state cookie - - oauth2Token, err := c.config.Exchange(ctx, r.URL.Query().Get("code")) - if err != nil { - resputil.JSON(w, err) - return - } - - // Extract the ID Token from OAuth2 token. - rawIDToken, ok := oauth2Token.Extra("id_token").(string) - if !ok { - resputil.JSON(w, err) - return - } - - // Parse and verify ID Token payload. - _, err = c.verifier.Verify(ctx, rawIDToken) - if err != nil { - resputil.JSON(w, err) - return - } - - u, err := c.provider.UserInfo(ctx, oauth2.StaticTokenSource(oauth2Token)) - if err != nil { - resputil.JSON(w, err) - return - } - p := &oidcProfile{} - u.Claims(p) - - var user = &types.User{ - SatosaID: p.Sub, - Email: p.Email, - Name: p.Name, - } - - if user, err = c.userService.With(ctx).FindOrCreate(user); err != nil { - resputil.JSON(w, err) - return - } else { - c.jwt.SetCookie(w, r, user) - } - - http.Redirect(w, r, c.appURL, http.StatusSeeOther) -} - -func (c *openIdConnect) stateCheck(r *http.Request) bool { - if cState, err := r.Cookie(openIdConnectStateCookie); err == nil { - rState := r.URL.Query().Get("state") - return len(rState) > 0 && cState.Value == rState - } - - return false -} - -// Sets state cookie -func (c *openIdConnect) setStateCookie(w http.ResponseWriter, r *http.Request, value string) { - var maxAge int - - if len(value) == 0 { - // When empty string for a value is received, - // set maxAge to -1. That will effectively delete the cookie - maxAge = -1 - } - - // Store state to cookie as well - http.SetCookie(w, &http.Cookie{ - Name: openIdConnectStateCookie, - Value: value, - Expires: time.Now().Add(time.Duration(c.stateCookieExpiry) * time.Minute), - MaxAge: maxAge, - HttpOnly: true, - Secure: r.URL.Scheme == "https", - Path: "/oidc", - Domain: c.stateCookieDomain, - }) -} diff --git a/system/rest/router.go b/system/rest/router.go index 0eba9eb96..ffbfb90d2 100644 --- a/system/rest/router.go +++ b/system/rest/router.go @@ -1,43 +1,16 @@ package rest import ( - "context" - "log" - "github.com/go-chi/chi" "github.com/crusttech/crust/internal/auth" - "github.com/crusttech/crust/internal/config" - "github.com/crusttech/crust/system/internal/repository" - "github.com/crusttech/crust/system/internal/service" "github.com/crusttech/crust/system/rest/handlers" ) -func MountRoutes(oidcConfig *config.OIDC, socialConfig *config.Social, jwtEncoder auth.TokenEncoder) func(chi.Router) { - var err error - var userSvc = service.User(context.Background()) - var ctx = context.Background() - var oidc *openIdConnect - - if oidcConfig.Enabled { - oidc, err = OpenIdConnect(ctx, oidcConfig, userSvc, jwtEncoder, repository.NewSettings(ctx, repository.DB(ctx))) - if err != nil { - log.Println("Could not initialize OIDC:", err.Error()) - } - } else { - log.Println("OIDC is disabled") - } - +func MountRoutes(jwtEncoder auth.TokenEncoder) func(chi.Router) { // Initialize handers & controllers. return func(r chi.Router) { - if oidcConfig.Enabled && oidc != nil { - r.Route("/oidc", func(r chi.Router) { - r.Get("/", oidc.HandleRedirect) - r.Get("/callback", oidc.HandleOAuth2Callback) - }) - } - - NewSocial(socialConfig, jwtEncoder).MountRoutes(r) + NewSocial(jwtEncoder).MountRoutes(r) // Provide raw `/auth` handlers Auth{}.New().Handlers(jwtEncoder).MountRoutes(r) diff --git a/system/rest/social.go b/system/rest/social.go deleted file mode 100644 index a5692bf40..000000000 --- a/system/rest/social.go +++ /dev/null @@ -1,177 +0,0 @@ -package rest - -import ( - "context" - "fmt" - "log" - "net/http" - "os" - "strings" - "time" - - "github.com/go-chi/chi" - "github.com/gorilla/sessions" - "github.com/markbates/goth" - "github.com/markbates/goth/gothic" - "github.com/markbates/goth/providers/facebook" - "github.com/markbates/goth/providers/github" - "github.com/markbates/goth/providers/gplus" - "github.com/markbates/goth/providers/linkedin" - "github.com/titpetric/factory/resputil" - - "github.com/crusttech/crust/internal/auth" - "github.com/crusttech/crust/internal/config" - "github.com/crusttech/crust/system/internal/service" -) - -type ( - Social struct { - auth service.AuthService - config *config.Social - jwtEncoder auth.TokenEncoder - } -) - -func NewSocial(config *config.Social, jwtEncoder auth.TokenEncoder) *Social { - return &Social{ - auth: service.DefaultAuth, - config: config, - jwtEncoder: jwtEncoder, - } -} - -func (ctrl *Social) MountRoutes(r chi.Router) { - store := sessions.NewCookieStore([]byte(ctrl.config.SessionStoreSecret)) - store.MaxAge(ctrl.config.SessionStoreExpiry) - store.Options.Path = "/social" - store.Options.HttpOnly = true - store.Options.Secure = false // @todo - gothic.Store = store - - getProviderConfig := func(provider string) (key, secret string, has bool) { - key, keyOk := os.LookupEnv("AUTH_SOCIAL_" + provider + "_KEY") - sec, secOk := os.LookupEnv("AUTH_SOCIAL_" + provider + "_SECRET") - if keyOk && secOk { - return key, sec, true - } else { - log.Print( - "Binding auth endpoints without " + provider + " provider " + - "(missing key/secret, check AUTH_" + provider + "_KEY, AUTH_" + provider + "_SECRET)") - } - - return "", "", false - } - - if key, sec, ok := getProviderConfig("FACEBOOK"); ok { - goth.UseProviders(facebook.New(key, sec, ctrl.config.Url+"/social/facebook/callback", "email")) - } - - if key, sec, ok := getProviderConfig("GPLUS"); ok { - goth.UseProviders(gplus.New(key, sec, ctrl.config.Url+"/social/gplus/callback", "email")) - } - - if key, sec, ok := getProviderConfig("GITHUB"); ok { - goth.UseProviders(github.New(key, sec, ctrl.config.Url+"/social/gplus/callback", "email")) - } - - if key, sec, ok := getProviderConfig("LINKEDIN"); ok { - goth.UseProviders(linkedin.New(key, sec, ctrl.config.Url+"/social/linkedin/callback", "email")) - } - - // Copy provider from path (Chi URL param) to request context and return it - copyProviderToContext := func(r *http.Request) *http.Request { - return r.WithContext(context.WithValue(r.Context(), "provider", chi.URLParam(r, "provider"))) - } - - r.Route("/social/{provider}", func(r chi.Router) { - r.Get("/", func(w http.ResponseWriter, r *http.Request) { - r = copyProviderToContext(r) - - // Always set redir cookie, even if not requested. If param is empty, cookie is removed - ctrl.setCookie(w, r, "redir", r.URL.Query().Get("redir")) - - // try to get the user without re-authenticating - if user, err := gothic.CompleteUserAuth(w, r); err != nil { - gothic.BeginAuthHandler(w, r) - } else { - // We've successfully singed-in through 3rd party auth - ctrl.handleSuccessfulAuth(w, r, user) - } - }) - - r.Get("/callback", func(w http.ResponseWriter, r *http.Request) { - r = copyProviderToContext(r) - - if user, err := gothic.CompleteUserAuth(w, r); err != nil { - log.Printf("Failed to complete user auth: %v", err) - ctrl.handleFailedCallback(w, r, err) - } else { - ctrl.handleSuccessfulAuth(w, r, user) - } - }) - - r.Get("/logout", func(w http.ResponseWriter, r *http.Request) { - if err := gothic.Logout(w, r); err != nil { - log.Printf("Failed to social logout: %v", err) - } - - w.Header().Set("Location", "/") - w.WriteHeader(http.StatusTemporaryRedirect) - }) - }) -} - -func (ctrl *Social) handleFailedCallback(w http.ResponseWriter, r *http.Request, err error) { - provider := chi.URLParam(r, "provider") - - if strings.Contains(err.Error(), "Error processing your OAuth request: Invalid oauth_verifier parameter") { - // Just take user through the same loop again - w.Header().Set("Location", "/social/"+provider) - w.WriteHeader(http.StatusSeeOther) - return - } - - fmt.Fprintf(w, "SSO Error: %v", err.Error()) - w.WriteHeader(http.StatusOK) -} - -// Handles authentication via external auth providers of -// unknown an user + appending authentication on external providers -// to a current user -func (ctrl *Social) handleSuccessfulAuth(w http.ResponseWriter, r *http.Request, cred goth.User) { - log.Printf("Successful social login: %v", cred) - - if u, err := ctrl.auth.With(r.Context()).Social(cred); err != nil { - resputil.JSON(w, err) - } else { - ctrl.jwtEncoder.SetCookie(w, r, u) - - if c, err := r.Cookie("redir"); c != nil && err == nil { - ctrl.setCookie(w, r, "redir", "") - w.Header().Set("Location", c.Value) - w.WriteHeader(http.StatusSeeOther) - - } - - resputil.JSON(w, u, err) - } -} - -// Extracts and authenticates JWT from context, validates claims -func (ctrl *Social) setCookie(w http.ResponseWriter, r *http.Request, name, value string) { - cookie := &http.Cookie{ - Name: name, - Expires: time.Now().Add(time.Duration(ctrl.config.SessionStoreExpiry) * time.Second), - Secure: r.URL.Scheme == "https", - Domain: r.URL.Hostname(), - Path: "/social", - } - - if value == "" { - cookie.Expires = time.Unix(0, 0) - } else { - cookie.Value = value - } - - http.SetCookie(w, cookie) -} diff --git a/system/rest/user.go b/system/rest/user.go index 29e3def4c..31b0d3a40 100644 --- a/system/rest/user.go +++ b/system/rest/user.go @@ -38,7 +38,7 @@ func (ctrl *User) Create(ctx context.Context, r *request.UserCreate) (interface{ Email: r.Email, Name: r.Name, Handle: r.Handle, - Kind: r.Kind, + Kind: types.UserKind(r.Kind), } return ctrl.user.With(ctx).Create(user) @@ -50,7 +50,7 @@ func (ctrl *User) Update(ctx context.Context, r *request.UserUpdate) (interface{ Email: r.Email, Name: r.Name, Handle: r.Handle, - Kind: r.Kind, + Kind: types.UserKind(r.Kind), } return ctrl.user.With(ctx).Update(user) diff --git a/system/routes.go b/system/routes.go index 531bbf2bc..4f61896d7 100644 --- a/system/routes.go +++ b/system/routes.go @@ -24,7 +24,7 @@ func MountRoutes(ctx context.Context, r chi.Router) { // Only protect application routes with JWT r.Group(func(r chi.Router) { r.Use(jwtVerifier, jwtAuthenticator) - mountRoutes(r, flags.http, rest.MountRoutes(flags.oidc, flags.social, jwtEncoder)) + mountRoutes(r, flags.http, rest.MountRoutes(jwtEncoder)) }) } diff --git a/system/start.go b/system/start.go index 0e554a2cb..d2b537be0 100644 --- a/system/start.go +++ b/system/start.go @@ -16,6 +16,7 @@ import ( "github.com/crusttech/crust/internal/metrics" "github.com/crusttech/crust/internal/settings" migrate "github.com/crusttech/crust/system/db" + "github.com/crusttech/crust/system/internal/auth/external" "github.com/crusttech/crust/system/internal/repository" "github.com/crusttech/crust/system/service" ) @@ -48,13 +49,6 @@ func Init(ctx context.Context) error { // Load settings from the database, // for now, only at start-up time. - ctx := context.Background() - // settingsRepository := internalRepository.NewSettings(repository.DB(ctx), "sys_settings") - // if ss, err := settingsRepository.Find(types.SettingsFilter{}); err != nil { - // panic(err) - // } else { - // spew.Dump(ss.KV()) - // } settingService := settings.NewService(settings.NewRepository(repository.DB(ctx), "sys_settings")) // configure resputil options @@ -66,8 +60,15 @@ func Init(ctx context.Context) error { }, }) - // Don't change this to init(), it needs Database - return service.Init() + // Don't change this, it needs database connection + if err := service.Init(); err != nil { + return err + } + + // Setup goth/social authentication + external.Init(settingService.With(ctx)) + + return nil } func InitDatabase(ctx context.Context) error { diff --git a/system/types/credentials.go b/system/types/credentials.go index 5b1d8e0a4..ac327c879 100644 --- a/system/types/credentials.go +++ b/system/types/credentials.go @@ -8,30 +8,18 @@ import ( type ( Credentials struct { - ID uint64 `json:"credentialsID,string" db:"id"` - OwnerID uint64 `json:"ownerID,string" db:"rel_owner"` - Label string `json:"label" db:"label"` - Kind CredentialsKind `json:"kind" db:"kind"` - Credentials string `json:"-" db:"credentials"` - Meta types.JSONText `json:"-" db:"meta"` - ExpiresAt *time.Time `json:"expiresAt,omitempty" db:"expires_at"` - CreatedAt time.Time `json:"createdAt,omitempty" db:"created_at"` - DeletedAt *time.Time `json:"deletedAt,omitempty" db:"deleted_at"` + ID uint64 `json:"credentialsID,string" db:"id"` + OwnerID uint64 `json:"ownerID,string" db:"rel_owner"` + Label string `json:"label" db:"label"` + Kind string `json:"kind" db:"kind"` + Credentials string `json:"-" db:"credentials"` + Meta types.JSONText `json:"-" db:"meta"` + LastUsedAt *time.Time `json:"lastUsedAt,omitempty" db:"last_used_at"` + ExpiresAt *time.Time `json:"expiresAt,omitempty" db:"expires_at"` + CreatedAt time.Time `json:"createdAt,omitempty" db:"created_at"` + UpdatedAt *time.Time `json:"updatedAt,omitempty" db:"updated_at"` + DeletedAt *time.Time `json:"deletedAt,omitempty" db:"deleted_at"` } - - CredentialsKind string -) - -const ( - // Use as a password for users or as API secret for bots (and credentials-id as a key) as a value for "credentials" - CredentialsKindHash CredentialsKind = "hash" - - // Identity (profile-id) stored under "credentials" - CredentialsKindFacebook CredentialsKind = "facebook" - CredentialsKindGPlus CredentialsKind = "gplus" - CredentialsKindGitHub CredentialsKind = "github" - CredentialsKindLinkedin CredentialsKind = "linkedin" - // CredentialsKindSatosa CredentialsKind = "satosa" ) func (u *Credentials) Valid() bool { diff --git a/system/types/user.go b/system/types/user.go index eeed22856..941bc3c2a 100644 --- a/system/types/user.go +++ b/system/types/user.go @@ -6,7 +6,6 @@ import ( "time" "github.com/pkg/errors" - "golang.org/x/crypto/bcrypt" "github.com/crusttech/crust/internal/rules" ) @@ -19,7 +18,6 @@ type ( Name string `json:"name" db:"name"` Handle string `json:"handle" db:"handle"` Kind UserKind `json:"kind" db:"kind"` - SatosaID string `json:"-" db:"satosa_id"` Meta *UserMeta `json:"meta" db:"meta"` @@ -27,8 +25,6 @@ type ( RelatedUserID uint64 `json:"relatedUserID,string" db:"rel_user_id"` User *User `json:"user" db:"-"` - Password []byte `json:"-" db:"password"` - CreatedAt time.Time `json:"createdAt,omitempty" db:"created_at"` UpdatedAt *time.Time `json:"updatedAt,omitempty" db:"updated_at"` SuspendedAt *time.Time `json:"suspendedAt,omitempty" db:"suspended_at"` @@ -64,20 +60,6 @@ func (u *User) Identity() uint64 { return u.ID } -func (u *User) ValidatePassword(password string) bool { - return bcrypt.CompareHashAndPassword(u.Password, []byte(password)) == nil -} - -func (u *User) GeneratePassword(password string) error { - pwd, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) - if err != nil { - return err - } - - u.Password = pwd - return nil -} - // Resource returns a resource ID for this type func (u *User) PermissionResource() rules.Resource { return UserPermissionResource.AppendID(u.ID) diff --git a/vendor/github.com/markbates/goth/providers/openidConnect/openidConnect.go b/vendor/github.com/markbates/goth/providers/openidConnect/openidConnect.go new file mode 100644 index 000000000..a4ff1d40f --- /dev/null +++ b/vendor/github.com/markbates/goth/providers/openidConnect/openidConnect.go @@ -0,0 +1,412 @@ +package openidConnect + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "github.com/markbates/goth" + "golang.org/x/oauth2" + "io/ioutil" + "net/http" + "strings" + "time" +) + +const ( + // Standard Claims http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims + // fixed, cannot be changed + subjectClaim = "sub" + expiryClaim = "exp" + audienceClaim = "aud" + issuerClaim = "iss" + + PreferredUsernameClaim = "preferred_username" + EmailClaim = "email" + NameClaim = "name" + NicknameClaim = "nickname" + PictureClaim = "picture" + GivenNameClaim = "given_name" + FamilyNameClaim = "family_name" + AddressClaim = "address" + + // Unused but available to set in Provider claims + MiddleNameClaim = "middle_name" + ProfileClaim = "profile" + WebsiteClaim = "website" + EmailVerifiedClaim = "email_verified" + GenderClaim = "gender" + BirthdateClaim = "birthdate" + ZoneinfoClaim = "zoneinfo" + LocaleClaim = "locale" + PhoneNumberClaim = "phone_number" + PhoneNumberVerifiedClaim = "phone_number_verified" + UpdatedAtClaim = "updated_at" + + clockSkew = 10 * time.Second +) + +// Provider is the implementation of `goth.Provider` for accessing OpenID Connect provider +type Provider struct { + ClientKey string + Secret string + CallbackURL string + HTTPClient *http.Client + config *oauth2.Config + openIDConfig *OpenIDConfig + providerName string + + UserIdClaims []string + NameClaims []string + NickNameClaims []string + EmailClaims []string + AvatarURLClaims []string + FirstNameClaims []string + LastNameClaims []string + LocationClaims []string + + SkipUserInfoRequest bool +} + +type OpenIDConfig struct { + AuthEndpoint string `json:"authorization_endpoint"` + TokenEndpoint string `json:"token_endpoint"` + UserInfoEndpoint string `json:"userinfo_endpoint"` + Issuer string `json:"issuer"` +} + +// New creates a new OpenID Connect provider, and sets up important connection details. +// You should always call `openidConnect.New` to get a new Provider. Never try to create +// one manually. +// New returns an implementation of an OpenID Connect Authorization Code Flow +// See http://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth +// ID Token decryption is not (yet) supported +// UserInfo decryption is not (yet) supported +func New(clientKey, secret, callbackURL, openIDAutoDiscoveryURL string, scopes ...string) (*Provider, error) { + p := &Provider{ + ClientKey: clientKey, + Secret: secret, + CallbackURL: callbackURL, + + UserIdClaims: []string{subjectClaim}, + NameClaims: []string{NameClaim}, + NickNameClaims: []string{NicknameClaim, PreferredUsernameClaim}, + EmailClaims: []string{EmailClaim}, + AvatarURLClaims: []string{PictureClaim}, + FirstNameClaims: []string{GivenNameClaim}, + LastNameClaims: []string{FamilyNameClaim}, + LocationClaims: []string{AddressClaim}, + + providerName: "openid-connect", + } + + openIDConfig, err := getOpenIDConfig(p, openIDAutoDiscoveryURL) + if err != nil { + return nil, err + } + p.openIDConfig = openIDConfig + + p.config = newConfig(p, scopes, openIDConfig) + return p, nil +} + +// Name is the name used to retrieve this provider later. +func (p *Provider) Name() string { + return p.providerName +} + +// SetName is to update the name of the provider (needed in case of multiple providers of 1 type) +func (p *Provider) SetName(name string) { + p.providerName = name +} + +func (p *Provider) Client() *http.Client { + return goth.HTTPClientWithFallBack(p.HTTPClient) +} + +// Debug is a no-op for the openidConnect package. +func (p *Provider) Debug(debug bool) {} + +// BeginAuth asks the OpenID Connect provider for an authentication end-point. +func (p *Provider) BeginAuth(state string) (goth.Session, error) { + url := p.config.AuthCodeURL(state) + session := &Session{ + AuthURL: url, + } + return session, nil +} + +// FetchUser will use the the id_token and access requested information about the user. +func (p *Provider) FetchUser(session goth.Session) (goth.User, error) { + sess := session.(*Session) + + expiresAt := sess.ExpiresAt + + if sess.IDToken == "" { + return goth.User{}, fmt.Errorf("%s cannot get user information without id_token", p.providerName) + } + + // decode returned id token to get expiry + claims, err := decodeJWT(sess.IDToken) + + if err != nil { + return goth.User{}, fmt.Errorf("oauth2: error decoding JWT token: %v", err) + } + + expiry, err := p.validateClaims(claims) + if err != nil { + return goth.User{}, fmt.Errorf("oauth2: error validating JWT token: %v", err) + } + + if expiry.Before(expiresAt) { + expiresAt = expiry + } + + if err := p.getUserInfo(sess.AccessToken, claims); err != nil { + return goth.User{}, err + } + + user := goth.User{ + AccessToken: sess.AccessToken, + Provider: p.Name(), + RefreshToken: sess.RefreshToken, + ExpiresAt: expiresAt, + RawData: claims, + } + + p.userFromClaims(claims, &user) + return user, err +} + +//RefreshTokenAvailable refresh token is provided by auth provider or not +func (p *Provider) RefreshTokenAvailable() bool { + return true +} + +//RefreshToken get new access token based on the refresh token +func (p *Provider) RefreshToken(refreshToken string) (*oauth2.Token, error) { + token := &oauth2.Token{RefreshToken: refreshToken} + ts := p.config.TokenSource(oauth2.NoContext, token) + newToken, err := ts.Token() + if err != nil { + return nil, err + } + return newToken, err +} + +// validate according to standard, returns expiry +// http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation +func (p *Provider) validateClaims(claims map[string]interface{}) (time.Time, error) { + audience := getClaimValue(claims, []string{audienceClaim}) + if audience != p.ClientKey { + found := false + audiences := getClaimValues(claims, []string{audienceClaim}) + for _, aud := range audiences { + if aud == p.ClientKey { + found = true + break + } + } + if !found { + return time.Time{}, errors.New("audience in token does not match client key") + } + } + + issuer := getClaimValue(claims, []string{issuerClaim}) + if issuer != p.openIDConfig.Issuer { + return time.Time{}, errors.New("issuer in token does not match issuer in OpenIDConfig discovery") + } + + // expiry is required for JWT, not for UserInfoResponse + // is actually a int64, so force it in to that type + expiryClaim := int64(claims[expiryClaim].(float64)) + expiry := time.Unix(expiryClaim, 0) + if expiry.Add(clockSkew).Before(time.Now()) { + return time.Time{}, errors.New("user info JWT token is expired") + } + return expiry, nil +} + +func (p *Provider) userFromClaims(claims map[string]interface{}, user *goth.User) { + // required + user.UserID = getClaimValue(claims, p.UserIdClaims) + + user.Name = getClaimValue(claims, p.NameClaims) + user.NickName = getClaimValue(claims, p.NickNameClaims) + user.Email = getClaimValue(claims, p.EmailClaims) + user.AvatarURL = getClaimValue(claims, p.AvatarURLClaims) + user.FirstName = getClaimValue(claims, p.FirstNameClaims) + user.LastName = getClaimValue(claims, p.LastNameClaims) + user.Location = getClaimValue(claims, p.LocationClaims) +} + +func (p *Provider) getUserInfo(accessToken string, claims map[string]interface{}) error { + // skip if there is no UserInfoEndpoint or is explicitly disabled + if p.openIDConfig.UserInfoEndpoint == "" || p.SkipUserInfoRequest { + return nil + } + + userInfoClaims, err := p.fetchUserInfo(p.openIDConfig.UserInfoEndpoint, accessToken) + if err != nil { + return err + } + + // The sub (subject) Claim MUST always be returned in the UserInfo Response. + // http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse + userInfoSubject := getClaimValue(userInfoClaims, []string{subjectClaim}) + if userInfoSubject == "" { + return fmt.Errorf("userinfo response did not contain a 'sub' claim: %#v", userInfoClaims) + } + + // The sub Claim in the UserInfo Response MUST be verified to exactly match the sub Claim in the ID Token; + // if they do not match, the UserInfo Response values MUST NOT be used. + // http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse + subject := getClaimValue(claims, []string{subjectClaim}) + if userInfoSubject != subject { + return fmt.Errorf("userinfo 'sub' claim (%s) did not match id_token 'sub' claim (%s)", userInfoSubject, subject) + } + + // Merge in userinfo claims in case id_token claims contained some that userinfo did not + for k, v := range userInfoClaims { + claims[k] = v + } + + return nil +} + +// fetch and decode JSON from the given UserInfo URL +func (p *Provider) fetchUserInfo(url, accessToken string) (map[string]interface{}, error) { + req, _ := http.NewRequest("GET", url, nil) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken)) + + resp, err := p.Client().Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("Non-200 response from UserInfo: %d, WWW-Authenticate=%s", resp.StatusCode, resp.Header.Get("WWW-Authenticate")) + } + + // The UserInfo Claims MUST be returned as the members of a JSON object + // http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + return unMarshal(data) +} + +func getOpenIDConfig(p *Provider, openIDAutoDiscoveryURL string) (*OpenIDConfig, error) { + res, err := p.Client().Get(openIDAutoDiscoveryURL) + if err != nil { + return nil, err + } + defer res.Body.Close() + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + return nil, err + } + + openIDConfig := &OpenIDConfig{} + err = json.Unmarshal(body, openIDConfig) + if err != nil { + return nil, err + } + + return openIDConfig, nil +} + +func newConfig(provider *Provider, scopes []string, openIDConfig *OpenIDConfig) *oauth2.Config { + c := &oauth2.Config{ + ClientID: provider.ClientKey, + ClientSecret: provider.Secret, + RedirectURL: provider.CallbackURL, + Endpoint: oauth2.Endpoint{ + AuthURL: openIDConfig.AuthEndpoint, + TokenURL: openIDConfig.TokenEndpoint, + }, + Scopes: []string{}, + } + + if len(scopes) > 0 { + foundOpenIDScope := false + + for _, scope := range scopes { + if scope == "openid" { + foundOpenIDScope = true + } + c.Scopes = append(c.Scopes, scope) + } + + if !foundOpenIDScope { + c.Scopes = append(c.Scopes, "openid") + } + } else { + c.Scopes = []string{"openid"} + } + + return c +} + +func getClaimValue(data map[string]interface{}, claims []string) string { + for _, claim := range claims { + if value, ok := data[claim]; ok { + if stringValue, ok := value.(string); ok && len(stringValue) > 0 { + return stringValue + } + } + } + + return "" +} + +func getClaimValues(data map[string]interface{}, claims []string) []string { + var result []string + + for _, claim := range claims { + if value, ok := data[claim]; ok { + if stringValues, ok := value.([]interface{}); ok { + for _, stringValue := range stringValues { + if s, ok := stringValue.(string); ok && len(s) > 0 { + result = append(result, s) + } + } + } + } + } + + return result +} + +// decodeJWT decodes a JSON Web Token into a simple map +// http://openid.net/specs/draft-jones-json-web-token-07.html +func decodeJWT(jwt string) (map[string]interface{}, error) { + jwtParts := strings.Split(jwt, ".") + if len(jwtParts) != 3 { + return nil, errors.New("jws: invalid token received, not all parts available") + } + + // Re-pad, if needed + encodedPayload := jwtParts[1] + if l := len(encodedPayload) % 4; l != 0 { + encodedPayload += strings.Repeat("=", 4-l) + } + + decodedPayload, err := base64.StdEncoding.DecodeString(encodedPayload) + if err != nil { + return nil, err + } + + return unMarshal(decodedPayload) +} + +func unMarshal(payload []byte) (map[string]interface{}, error) { + data := make(map[string]interface{}) + + return data, json.NewDecoder(bytes.NewBuffer(payload)).Decode(&data) +} diff --git a/vendor/github.com/markbates/goth/providers/openidConnect/session.go b/vendor/github.com/markbates/goth/providers/openidConnect/session.go new file mode 100644 index 000000000..d223cf875 --- /dev/null +++ b/vendor/github.com/markbates/goth/providers/openidConnect/session.go @@ -0,0 +1,63 @@ +package openidConnect + +import ( + "encoding/json" + "errors" + "github.com/markbates/goth" + "golang.org/x/oauth2" + "strings" + "time" +) + +// Session stores data during the auth process with the OpenID Connect provider. +type Session struct { + AuthURL string + AccessToken string + RefreshToken string + ExpiresAt time.Time + IDToken string +} + +// GetAuthURL will return the URL set by calling the `BeginAuth` function on the OpenID Connect provider. +func (s Session) GetAuthURL() (string, error) { + if s.AuthURL == "" { + return "", errors.New("an AuthURL has not be set") + } + return s.AuthURL, nil +} + +// Authorize the session with the OpenID Connect provider and return the access token to be stored for future use. +func (s *Session) Authorize(provider goth.Provider, params goth.Params) (string, error) { + p := provider.(*Provider) + token, err := p.config.Exchange(oauth2.NoContext, params.Get("code")) + if err != nil { + return "", err + } + + if !token.Valid() { + return "", errors.New("Invalid token received from provider") + } + + s.AccessToken = token.AccessToken + s.RefreshToken = token.RefreshToken + s.ExpiresAt = token.Expiry + s.IDToken = token.Extra("id_token").(string) + return token.AccessToken, err +} + +// Marshal the session into a string +func (s Session) Marshal() string { + b, _ := json.Marshal(s) + return string(b) +} + +func (s Session) String() string { + return s.Marshal() +} + +// UnmarshalSession will unmarshal a JSON string into a session. +func (p *Provider) UnmarshalSession(data string) (goth.Session, error) { + sess := &Session{} + err := json.NewDecoder(strings.NewReader(data)).Decode(sess) + return sess, err +}