diff --git a/.gitignore b/.gitignore index b566459860..9603e7486e 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,10 @@ node_modules assets/bundle*.* assets/*@*.svg assets/*@*.png +assets/*@*.eot +assets/*@*.woff +assets/*@*.woff2 +assets/*@*.ttf frontend/templates/react.tmpl bindata.go *.cover @@ -23,4 +27,4 @@ tmp/ .DS_Store # test mysql server data -mysqldata/ \ No newline at end of file +mysqldata/ diff --git a/cmd/fleet/serve.go b/cmd/fleet/serve.go index 8652bf57cb..131c6e913f 100644 --- a/cmd/fleet/serve.go +++ b/cmd/fleet/serve.go @@ -9,6 +9,8 @@ import ( "net/url" "os" "os/signal" + "regexp" + "strings" "syscall" "time" @@ -27,12 +29,15 @@ import ( "github.com/kolide/fleet/server/service" "github.com/kolide/fleet/server/sso" "github.com/kolide/kit/version" + "github.com/pkg/errors" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/spf13/cobra" "google.golang.org/grpc" ) +var allowedURLPrefixRegexp = regexp.MustCompile("^(?:/[a-zA-Z0-9_.~-]+)+$") + type initializer interface { // Initialize is used to populate a datastore with // preloaded data @@ -68,6 +73,7 @@ the way that the Fleet server works. logger = kitlog.With(logger, "ts", kitlog.DefaultTimestampUTC) } + // Check for deprecated config options. if config.Osquery.StatusLogFile != "" { level.Info(logger).Log( "DEPRECATED", "use filesystem.status_log_file.", @@ -90,6 +96,21 @@ the way that the Fleet server works. config.Filesystem.EnableLogRotation = config.Osquery.EnableLogRotation } + if len(config.Server.URLPrefix) > 0 { + // Massage provided prefix to match expected format + config.Server.URLPrefix = strings.TrimSuffix(config.Server.URLPrefix, "/") + if len(config.Server.URLPrefix) > 0 && !strings.HasPrefix(config.Server.URLPrefix, "/") { + config.Server.URLPrefix = "/" + config.Server.URLPrefix + } + + if !allowedURLPrefixRegexp.MatchString(config.Server.URLPrefix) { + initFatal( + errors.Errorf("prefix must match regexp \"%s\"", allowedURLPrefixRegexp.String()), + "setting server URL prefix", + ) + } + } + var ds kolide.Datastore var err error mailService := mail.NewService() @@ -190,8 +211,8 @@ the way that the Fleet server works. var apiHandler, frontendHandler http.Handler { - frontendHandler = prometheus.InstrumentHandler("get_frontend", service.ServeFrontend(httpLogger)) - apiHandler = service.MakeHandler(svc, config.Auth.JwtKey, httpLogger) + frontendHandler = prometheus.InstrumentHandler("get_frontend", service.ServeFrontend(config.Server.URLPrefix, httpLogger)) + apiHandler = service.MakeHandler(svc, config, httpLogger) setupRequired, err := service.RequireSetup(svc) if err != nil { @@ -202,9 +223,9 @@ the way that the Fleet server works. // more efficient after the first startup. if setupRequired { apiHandler = service.WithSetup(svc, logger, apiHandler) - frontendHandler = service.RedirectLoginToSetup(svc, logger, frontendHandler) + frontendHandler = service.RedirectLoginToSetup(svc, logger, frontendHandler, config.Server.URLPrefix) } else { - frontendHandler = service.RedirectSetupToLogin(svc, logger, frontendHandler) + frontendHandler = service.RedirectSetupToLogin(svc, logger, frontendHandler, config.Server.URLPrefix) } } @@ -229,14 +250,13 @@ the way that the Fleet server works. // Instantiate a gRPC service to handle launcher requests. launcher := launcher.New(svc, logger, grpc.NewServer(), healthCheckers) - r := http.NewServeMux() - - r.Handle("/healthz", prometheus.InstrumentHandler("healthz", health.Handler(httpLogger, healthCheckers))) - r.Handle("/version", prometheus.InstrumentHandler("version", version.Handler())) - r.Handle("/assets/", prometheus.InstrumentHandler("static_assets", service.ServeStaticAssets("/assets/"))) - r.Handle("/metrics", prometheus.InstrumentHandler("metrics", promhttp.Handler())) - r.Handle("/api/", apiHandler) - r.Handle("/", frontendHandler) + rootMux := http.NewServeMux() + rootMux.Handle("/healthz", prometheus.InstrumentHandler("healthz", health.Handler(httpLogger, healthCheckers))) + rootMux.Handle("/version", prometheus.InstrumentHandler("version", version.Handler())) + rootMux.Handle("/assets/", prometheus.InstrumentHandler("static_assets", service.ServeStaticAssets("/assets/"))) + rootMux.Handle("/metrics", prometheus.InstrumentHandler("metrics", promhttp.Handler())) + rootMux.Handle("/api/", apiHandler) + rootMux.Handle("/", frontendHandler) if path, ok := os.LookupEnv("KOLIDE_TEST_PAGE_PATH"); ok { // test that we can load this @@ -244,7 +264,7 @@ the way that the Fleet server works. if err != nil { initFatal(err, "loading KOLIDE_TEST_PAGE_PATH") } - r.HandleFunc("/test", func(rw http.ResponseWriter, req *http.Request) { + rootMux.HandleFunc("/test", func(rw http.ResponseWriter, req *http.Request) { testPage, err := ioutil.ReadFile(path) if err != nil { rw.WriteHeader(http.StatusNotFound) @@ -262,13 +282,20 @@ the way that the Fleet server works. if err != nil { initFatal(err, "generating debug token") } - r.Handle("/debug/", http.StripPrefix("/debug/", netbug.AuthHandler(debugToken))) + rootMux.Handle("/debug/", http.StripPrefix("/debug/", netbug.AuthHandler(debugToken))) fmt.Printf("*** Debug mode enabled ***\nAccess the debug endpoints at /debug/?token=%s\n", url.QueryEscape(debugToken)) } + if len(config.Server.URLPrefix) > 0 { + + prefixMux := http.NewServeMux() + prefixMux.Handle(config.Server.URLPrefix+"/", http.StripPrefix(config.Server.URLPrefix, rootMux)) + rootMux = prefixMux + } + srv := &http.Server{ Addr: config.Server.Address, - Handler: launcher.Handler(r), + Handler: launcher.Handler(rootMux), ReadTimeout: 25 * time.Second, WriteTimeout: 40 * time.Second, ReadHeaderTimeout: 5 * time.Second, diff --git a/cmd/fleetctl/api.go b/cmd/fleetctl/api.go index 10f65ed40c..6370a8a83d 100644 --- a/cmd/fleetctl/api.go +++ b/cmd/fleetctl/api.go @@ -27,7 +27,7 @@ func unauthenticatedClientFromCLI(c *cli.Context) (*service.Client, error) { return nil, errors.New("set the Fleet API address with: fleetctl config set --address https://localhost:8080") } - fleet, err := service.NewClient(cc.Address, cc.TLSSkipVerify, cc.RootCA) + fleet, err := service.NewClient(cc.Address, cc.TLSSkipVerify, cc.RootCA, cc.URLPrefix) if err != nil { return nil, errors.Wrap(err, "error creating Fleet API client handler") } diff --git a/cmd/fleetctl/config.go b/cmd/fleetctl/config.go index 3dc78cb2b6..49f631e324 100644 --- a/cmd/fleetctl/config.go +++ b/cmd/fleetctl/config.go @@ -27,6 +27,7 @@ type Context struct { Token string `json:"token"` TLSSkipVerify bool `json:"tls-skip-verify"` RootCA string `json:"rootca"` + URLPrefix string `json:"url-prefix"` } func configFlag() cli.Flag { @@ -122,6 +123,8 @@ func getConfigValue(c *cli.Context, key string) (interface{}, error) { } else { return false, nil } + case "url-prefix": + return currentContext.URLPrefix, nil default: return nil, fmt.Errorf("%q is an invalid key", key) } @@ -166,6 +169,8 @@ func setConfigValue(c *cli.Context, key, value string) error { return errors.Wrapf(err, "error parsing %q as bool", value) } currentContext.TLSSkipVerify = boolValue + case "url-prefix": + currentContext.URLPrefix = value default: return fmt.Errorf("%q is an invalid option", key) } @@ -186,6 +191,7 @@ func configSetCommand() cli.Command { flToken string flTLSSkipVerify bool flRootCA string + flURLPrefix string ) return cli.Command{ Name: "set", @@ -228,6 +234,13 @@ func configSetCommand() cli.Command { Destination: &flRootCA, Usage: "Specify RootCA chain used to communicate with fleet", }, + cli.StringFlag{ + Name: "url-prefix", + EnvVar: "URL_PREFIX", + Value: "", + Destination: &flURLPrefix, + Usage: "Specify URL Prefix to use with Fleet server (copy from server configuration)", + }, }, Action: func(c *cli.Context) error { set := false @@ -272,6 +285,14 @@ func configSetCommand() cli.Command { fmt.Printf("[+] Set the rootca config key to %q in the %q context\n", flRootCA, c.String("context")) } + if flURLPrefix != "" { + set = true + if err := setConfigValue(c, "url-prefix", flURLPrefix); err != nil { + return errors.Wrap(err, "error setting URL Prefix") + } + fmt.Printf("[+] Set the url-prefix config key to %q in the %q context\n", flURLPrefix, c.String("context")) + } + if !set { return cli.ShowCommandHelp(c, "set") } diff --git a/docs/infrastructure/configuring-the-fleet-binary.md b/docs/infrastructure/configuring-the-fleet-binary.md index 612d264e44..6b9dd87041 100644 --- a/docs/infrastructure/configuring-the-fleet-binary.md +++ b/docs/infrastructure/configuring-the-fleet-binary.md @@ -351,6 +351,21 @@ Configures the TLS settings for compatibility with various user agents. Options tls_compatibility: intermediate ``` +##### `server_url_prefix` + +Sets a URL prefix to use when serving the Fleet API and frontend. Prefixes should be in the form `/apps/fleet` (no trailing slash). + +Note that some other configurations may need to be changed when modifying the URL prefix. In particular, URLs that are provided to osquery via flagfile, the configuration served by Fleet, the URL prefix used by `fleetctl`, and the redirect URL set with an SSO Identity Provider. + +- Default value: Empty (no prefix set) +- Environment variable: `KOLIDE_SERVER_URL_PREFIX` +- Config file format: + + ``` + server: + url_prefix: /apps/fleet + ``` + #### Auth @@ -771,4 +786,3 @@ The identifier of the pubsub topic that osquery status logs will be published to pubsub: status_topic: osquery_status ``` - diff --git a/frontend/components/AuthenticationFormWrapper/AuthenticationFormWrapper.jsx b/frontend/components/AuthenticationFormWrapper/AuthenticationFormWrapper.jsx index 0b800f785a..ee81af4d74 100644 --- a/frontend/components/AuthenticationFormWrapper/AuthenticationFormWrapper.jsx +++ b/frontend/components/AuthenticationFormWrapper/AuthenticationFormWrapper.jsx @@ -1,6 +1,8 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; +import logoVertical from '../../../assets/images/kolide-logo-vertical.svg'; + const baseClass = 'auth-form-wrapper'; class AuthenticationFormWrapper extends Component { @@ -13,7 +15,7 @@ class AuthenticationFormWrapper extends Component { return (
- Kolide Fleet + Kolide Fleet {children}
); diff --git a/frontend/components/Avatar/_styles.scss b/frontend/components/Avatar/_styles.scss index ef6f1103f0..95d84b0088 100644 --- a/frontend/components/Avatar/_styles.scss +++ b/frontend/components/Avatar/_styles.scss @@ -1,5 +1,5 @@ .avatar { - background: $white url('/assets/images/avatar-default.png') center 100% no-repeat; + background: $white url('../assets/images/avatar-default.png') center 100% no-repeat; background-size: cover; border-radius: 50%; diff --git a/frontend/components/LoginRoutes/LoginRoutes.jsx b/frontend/components/LoginRoutes/LoginRoutes.jsx index fa37cabd45..89d0c66e30 100644 --- a/frontend/components/LoginRoutes/LoginRoutes.jsx +++ b/frontend/components/LoginRoutes/LoginRoutes.jsx @@ -60,8 +60,8 @@ const mapStateToProps = (state, ownProps) => { const { location: { pathname, query } } = ownProps; const { token } = query; - const isForgotPassPage = pathname === '/login/forgot'; - const isResetPassPage = pathname === '/login/reset'; + const isForgotPassPage = pathname.endsWith('/login/forgot'); + const isResetPassPage = pathname.endsWith('/login/reset'); return { isForgotPassPage, diff --git a/frontend/components/queries/QueryPageWrapper/helpers.js b/frontend/components/queries/QueryPageWrapper/helpers.js index 7032a9c313..252ae1fad0 100644 --- a/frontend/components/queries/QueryPageWrapper/helpers.js +++ b/frontend/components/queries/QueryPageWrapper/helpers.js @@ -1,6 +1,7 @@ import { push } from 'react-router-redux'; import { join, omit, values } from 'lodash'; +import PATHS from 'router/paths'; import queryActions from 'redux/nodes/entities/queries/actions'; import { renderFlash } from 'redux/nodes/notifications/actions'; @@ -9,7 +10,7 @@ export const fetchQuery = (dispatch, queryID) => { .catch((errors) => { const errorMessage = join(values(omit(errors, 'http_status')), ', '); - dispatch(push('/queries/new')); + dispatch(push(PATHS.NEW_QUERY)); dispatch(renderFlash('error', errorMessage)); return false; diff --git a/frontend/components/side_panels/SiteNavHeader/UserMenu.jsx b/frontend/components/side_panels/SiteNavHeader/UserMenu.jsx index 1d97bc438f..2354f05d89 100644 --- a/frontend/components/side_panels/SiteNavHeader/UserMenu.jsx +++ b/frontend/components/side_panels/SiteNavHeader/UserMenu.jsx @@ -4,6 +4,7 @@ import classnames from 'classnames'; import Avatar from 'components/Avatar'; import Icon from 'components/icons/Icon'; +import PATHS from 'router/paths'; class UserMenu extends Component { static propTypes = { @@ -39,7 +40,7 @@ class UserMenu extends Component { diff --git a/frontend/components/side_panels/SiteNavSidePanel/navItems.js b/frontend/components/side_panels/SiteNavSidePanel/navItems.js index 09705aeec6..9a45f48d0e 100644 --- a/frontend/components/side_panels/SiteNavSidePanel/navItems.js +++ b/frontend/components/side_panels/SiteNavSidePanel/navItems.js @@ -1,27 +1,30 @@ +import PATHS from 'router/paths'; +import URL_PREFIX from 'router/url_prefix'; + export default (admin) => { const adminNavItems = [ { icon: 'admin', name: 'Admin', location: { - regex: /^\/admin/, - pathname: '/admin/users', + regex: new RegExp(`^${URL_PREFIX}/admin/`), + pathname: PATHS.ADMIN_USERS, }, subItems: [ { icon: 'admin', name: 'Manage Users', location: { - regex: /\/admin\/users/, - pathname: '/admin/users', + regex: new RegExp(`^${PATHS.ADMIN_USERS}`), + pathname: PATHS.ADMIN_USERS, }, }, { icon: 'user-settings', name: 'App Settings', location: { - regex: /\/admin\/settings/, - pathname: '/admin/settings', + regex: new RegExp(`^${PATHS.ADMIN_SETTINGS}`), + pathname: PATHS.ADMIN_SETTINGS, }, }, ], @@ -33,8 +36,8 @@ export default (admin) => { icon: 'hosts', name: 'Hosts', location: { - regex: /^\/hosts/, - pathname: '/hosts/manage', + regex: new RegExp(`^${URL_PREFIX}/hosts/`), + pathname: PATHS.MANAGE_HOSTS, }, subItems: [], }, @@ -42,24 +45,24 @@ export default (admin) => { icon: 'query', name: 'Query', location: { - regex: /^\/queries/, - pathname: '/queries/manage', + regex: new RegExp(`^${URL_PREFIX}/queries/`), + pathname: PATHS.MANAGE_QUERIES, }, subItems: [ { icon: 'query', name: 'Manage Queries', location: { - regex: /\/queries\/manage/, - pathname: '/queries/manage', + regex: new RegExp(`^${PATHS.MANAGE_QUERIES}`), + pathname: PATHS.MANAGE_QUERIES, }, }, { icon: 'pencil', name: 'New Query', location: { - regex: /\/queries\/new/, - pathname: '/queries/new', + regex: new RegExp(`^${PATHS.NEW_QUERY}`), + pathname: PATHS.NEW_QUERY, }, }, ], @@ -68,24 +71,24 @@ export default (admin) => { icon: 'packs', name: 'Packs', location: { - regex: /^\/packs/, - pathname: '/packs/manage', + regex: new RegExp(`^${URL_PREFIX}/packs/`), + pathname: PATHS.MANAGE_PACKS, }, subItems: [ { icon: 'packs', name: 'Manage Packs', location: { - regex: /\/packs\/manage/, - pathname: '/packs/manage', + regex: new RegExp(`^${PATHS.MANAGE_PACKS}`), + pathname: PATHS.MANAGE_PACKS, }, }, { icon: 'pencil', name: 'New Pack', location: { - regex: /\/packs\/new/, - pathname: '/packs/new', + regex: new RegExp(`^${PATHS.NEW_PACK}`), + pathname: PATHS.NEW_PACK, }, }, ], diff --git a/frontend/index.jsx b/frontend/index.jsx index d67d9d45b2..f0b0c36c7a 100644 --- a/frontend/index.jsx +++ b/frontend/index.jsx @@ -1,5 +1,6 @@ import ReactDOM from 'react-dom'; +import './public-path'; import routes from './router'; import './index.scss'; diff --git a/frontend/kolide/base.js b/frontend/kolide/base.js index 6361e98bbc..c08b730008 100644 --- a/frontend/kolide/base.js +++ b/frontend/kolide/base.js @@ -1,11 +1,13 @@ import local from 'utilities/local'; import Request from 'kolide/request'; +import URL_PREFIX from 'router/url_prefix'; + class Base { constructor () { const { origin } = global.window.location; - this.baseURL = `${origin}/api`; + this.baseURL = `${origin}${URL_PREFIX}/api`; this.bearerToken = local.getItem('auth_token'); } diff --git a/frontend/pages/ResetPasswordPage/ResetPasswordPage.jsx b/frontend/pages/ResetPasswordPage/ResetPasswordPage.jsx index 21ad5b6bb8..1d285bd0e3 100644 --- a/frontend/pages/ResetPasswordPage/ResetPasswordPage.jsx +++ b/frontend/pages/ResetPasswordPage/ResetPasswordPage.jsx @@ -10,6 +10,7 @@ import ResetPasswordForm from 'components/forms/ResetPasswordForm'; import StackedWhiteBoxes from 'components/StackedWhiteBoxes'; import { performRequiredPasswordReset } from 'redux/nodes/auth/actions'; import userInterface from 'interfaces/user'; +import PATHS from 'router/paths'; export class ResetPasswordPage extends Component { static propTypes = { @@ -30,7 +31,7 @@ export class ResetPasswordPage extends Component { const { dispatch, token, user } = this.props; if (!user && !token) { - return dispatch(push('/login')); + return dispatch(push(PATHS.LOGIN)); } return false; @@ -59,9 +60,7 @@ export class ResetPasswordPage extends Component { }; return dispatch(resetPassword(resetPasswordData)) - .then(() => { - return dispatch(push('/login')); - }) + .then(() => { return dispatch(push(PATHS.LOGIN)); }) .catch(() => false); }) @@ -77,7 +76,7 @@ export class ResetPasswordPage extends Component { const passwordUpdateParams = { password }; return dispatch(performRequiredPasswordReset(passwordUpdateParams)) - .then(() => { return dispatch(push('/')); }) + .then(() => { return dispatch(push(PATHS.HOME)); }) .catch(() => false); } diff --git a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.jsx b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.jsx index 3fb4265126..c6e59eee4d 100644 --- a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.jsx +++ b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.jsx @@ -28,7 +28,7 @@ import hostActions from 'redux/nodes/entities/hosts/actions'; import labelActions from 'redux/nodes/entities/labels/actions'; import { renderFlash } from 'redux/nodes/notifications/actions'; import entityGetter from 'redux/utilities/entityGetter'; -import paths from 'router/paths'; +import PATHS from 'router/paths'; import deepDifference from 'utilities/deep_difference'; import iconClassForLabel from 'utilities/icon_class_for_label'; import platformIconClass from 'utilities/platform_icon_class'; @@ -99,7 +99,7 @@ export class ManageHostsPage extends PureComponent { const { dispatch } = this.props; - dispatch(push(`/hosts/manage${NEW_LABEL_HASH}`)); + dispatch(push(`${PATHS.MANAGE_HOSTS}${NEW_LABEL_HASH}`)); return false; } @@ -107,7 +107,7 @@ export class ManageHostsPage extends PureComponent { onCancelAddLabel = () => { const { dispatch } = this.props; - dispatch(push('/hosts/manage')); + dispatch(push(PATHS.MANAGE_HOSTS)); return false; } @@ -168,7 +168,7 @@ export class ManageHostsPage extends PureComponent { evt.preventDefault(); const { dispatch } = this.props; - const { MANAGE_HOSTS } = paths; + const { MANAGE_HOSTS } = PATHS; const { slug } = selectedLabel; const nextLocation = slug === 'all-hosts' ? MANAGE_HOSTS : `${MANAGE_HOSTS}/${slug}`; @@ -217,7 +217,7 @@ export class ManageHostsPage extends PureComponent { return dispatch(labelActions.create(formData)) .then(() => { - dispatch(push('/hosts/manage')); + dispatch(push(PATHS.MANAGE_HOSTS)); return false; }); @@ -236,7 +236,7 @@ export class ManageHostsPage extends PureComponent { onDeleteLabel = () => { const { toggleDeleteLabelModal } = this; const { dispatch, selectedLabel } = this.props; - const { MANAGE_HOSTS } = paths; + const { MANAGE_HOSTS } = PATHS; return dispatch(labelActions.destroy(selectedLabel)) .then(() => { @@ -251,7 +251,7 @@ export class ManageHostsPage extends PureComponent { evt.preventDefault(); const { dispatch } = this.props; - const { NEW_QUERY } = paths; + const { NEW_QUERY } = PATHS; dispatch(push({ pathname: NEW_QUERY, diff --git a/frontend/pages/packs/AllPacksPage/AllPacksPage.jsx b/frontend/pages/packs/AllPacksPage/AllPacksPage.jsx index 829dcbe67e..6cfc73ef92 100644 --- a/frontend/pages/packs/AllPacksPage/AllPacksPage.jsx +++ b/frontend/pages/packs/AllPacksPage/AllPacksPage.jsx @@ -15,10 +15,11 @@ import PackDetailsSidePanel from 'components/side_panels/PackDetailsSidePanel'; import PackInfoSidePanel from 'components/side_panels/PackInfoSidePanel'; import packInterface from 'interfaces/pack'; import PacksList from 'components/packs/PacksList'; -import paths from 'router/paths'; import { renderFlash } from 'redux/nodes/notifications/actions'; import scheduledQueryActions from 'redux/nodes/entities/scheduled_queries/actions'; import scheduledQueryInterface from 'interfaces/scheduled_query'; +import PATHS from 'router/paths'; + const baseClass = 'all-packs-page'; @@ -137,7 +138,7 @@ export class AllPacksPage extends Component { onSelectPack = (selectedPack) => { const { dispatch } = this.props; const locationObject = { - pathname: '/packs/manage', + pathname: PATHS.MANAGE_PACKS, query: { selectedPack: selectedPack.id }, }; @@ -149,7 +150,7 @@ export class AllPacksPage extends Component { onDoubleClickPack = (selectedPack) => { const { dispatch } = this.props; - dispatch(push(`/packs/${selectedPack.id}`)); + dispatch(push(PATHS.PACK(selectedPack))); return false; } @@ -205,9 +206,8 @@ export class AllPacksPage extends Component { goToNewPackPage = () => { const { dispatch } = this.props; - const { NEW_PACK } = paths; - dispatch(push(NEW_PACK)); + dispatch(push(PATHS.NEW_PACK)); return false; } diff --git a/frontend/pages/packs/EditPackPage/EditPackPage.jsx b/frontend/pages/packs/EditPackPage/EditPackPage.jsx index ef50fee842..06f70d460b 100644 --- a/frontend/pages/packs/EditPackPage/EditPackPage.jsx +++ b/frontend/pages/packs/EditPackPage/EditPackPage.jsx @@ -19,6 +19,7 @@ import ScheduledQueriesListWrapper from 'components/queries/ScheduledQueriesList import { renderFlash } from 'redux/nodes/notifications/actions'; import scheduledQueryActions from 'redux/nodes/entities/scheduled_queries/actions'; import stateEntityGetter from 'redux/utilities/entityGetter'; +import PATHS from 'router/paths'; const baseClass = 'edit-pack-page'; @@ -110,7 +111,7 @@ export class EditPackPage extends Component { return false; } - return dispatch(push(`/packs/${packID}`)); + return dispatch(push(PATHS.PACK({ id: packID }))); } onFetchTargets = (query, targetsResponse) => { @@ -145,17 +146,17 @@ export class EditPackPage extends Component { onDblClickScheduledQuery = (scheduledQueryId) => { const { dispatch } = this.props; - return dispatch(push(`/queries/${scheduledQueryId}`)); + return dispatch(push(PATHS.EDIT_QUERY({ id: scheduledQueryId }))); } onToggleEdit = () => { const { dispatch, isEdit, packID } = this.props; if (isEdit) { - return dispatch(push(`/packs/${packID}`)); + return dispatch(push(PATHS.PACK({ id: packID }))); } - return dispatch(push(`/packs/${packID}/edit`)); + return dispatch(push(PATHS.EDIT_PACK({ id: packID }))); } onUpdateScheduledQuery = (formData) => { diff --git a/frontend/pages/packs/PackComposerPage/PackComposerPage.jsx b/frontend/pages/packs/PackComposerPage/PackComposerPage.jsx index a87bd7b9f2..859b0568f6 100644 --- a/frontend/pages/packs/PackComposerPage/PackComposerPage.jsx +++ b/frontend/pages/packs/PackComposerPage/PackComposerPage.jsx @@ -7,6 +7,7 @@ import { push } from 'react-router-redux'; import packActions from 'redux/nodes/entities/packs/actions'; import PackForm from 'components/forms/packs/PackForm'; import PackInfoSidePanel from 'components/side_panels/PackInfoSidePanel'; +import PATHS from 'router/paths'; const baseClass = 'pack-composer'; @@ -39,7 +40,7 @@ export class PackComposerPage extends Component { visitPackPage = (packID) => { const { dispatch } = this.props; - dispatch(push(`/packs/${packID}`)); + dispatch(push(PATHS.PACK({ id: packID }))); return false; } diff --git a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.jsx b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.jsx index a841dd61cc..2f5b7f3c83 100644 --- a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.jsx +++ b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.jsx @@ -11,7 +11,7 @@ import Modal from 'components/modals/Modal'; import NumberPill from 'components/NumberPill'; import Icon from 'components/icons/Icon'; import PackInfoSidePanel from 'components/side_panels/PackInfoSidePanel'; -import paths from 'router/paths'; +import PATHS from 'router/paths'; import QueryDetailsSidePanel from 'components/side_panels/QueryDetailsSidePanel'; import QueriesList from 'components/queries/QueriesList'; import queryActions from 'redux/nodes/entities/queries/actions'; @@ -114,7 +114,7 @@ export class ManageQueriesPage extends Component { onSelectQuery = (selectedQuery) => { const { dispatch } = this.props; const locationObject = { - pathname: '/queries/manage', + pathname: PATHS.MANAGE_QUERIES, query: { selectedQuery: selectedQuery.id }, }; @@ -126,7 +126,7 @@ export class ManageQueriesPage extends Component { onDblClickQuery = (selectedQuery) => { const { dispatch } = this.props; - dispatch(push(`/queries/${selectedQuery.id}`)); + dispatch(push(PATHS.EDIT_QUERY(selectedQuery))); return false; } @@ -162,7 +162,7 @@ export class ManageQueriesPage extends Component { goToNewQueryPage = () => { const { dispatch } = this.props; - const { NEW_QUERY } = paths; + const { NEW_QUERY } = PATHS; dispatch(push(NEW_QUERY)); @@ -171,7 +171,7 @@ export class ManageQueriesPage extends Component { goToEditQueryPage = (query) => { const { dispatch } = this.props; - const { EDIT_QUERY } = paths; + const { EDIT_QUERY } = PATHS; dispatch(push(EDIT_QUERY(query))); @@ -308,4 +308,3 @@ const mapStateToProps = (state, { location }) => { }; export default connect(mapStateToProps)(ManageQueriesPage); - diff --git a/frontend/pages/queries/QueryPage/QueryPage.jsx b/frontend/pages/queries/QueryPage/QueryPage.jsx index 814e57100f..e9579cfd6c 100644 --- a/frontend/pages/queries/QueryPage/QueryPage.jsx +++ b/frontend/pages/queries/QueryPage/QueryPage.jsx @@ -30,6 +30,7 @@ import { toggleSmallNav } from 'redux/nodes/app/actions'; import { selectOsqueryTable, setSelectedTargets, setSelectedTargetsQuery } from 'redux/nodes/components/QueryPages/actions'; import targetInterface from 'interfaces/target'; import validateQuery from 'components/forms/validators/validate_query'; +import PATHS from 'router/paths'; const baseClass = 'query-page'; const DEFAULT_CAMPAIGN = { @@ -280,7 +281,7 @@ export class QueryPage extends Component { return dispatch(queryActions.create(formData)) .then((query) => { - dispatch(push(`/queries/${query.id}`)); + dispatch(push(PATHS.EDIT_QUERY(query))); }) .catch(() => false); }) diff --git a/frontend/public-path.js b/frontend/public-path.js new file mode 100644 index 0000000000..bab53f9416 --- /dev/null +++ b/frontend/public-path.js @@ -0,0 +1,4 @@ +import URL_PREFIX from 'router/url_prefix'; + +// Sets the path used to load assets +__webpack_public_path__ = `${URL_PREFIX}/assets/`; // eslint-disable-line camelcase, no-undef diff --git a/frontend/router/index.jsx b/frontend/router/index.jsx index 1faf055d9e..e9d16314dc 100644 --- a/frontend/router/index.jsx +++ b/frontend/router/index.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import { browserHistory, IndexRedirect, IndexRoute, Redirect, Route, Router } from 'react-router'; +import { browserHistory, IndexRedirect, IndexRoute, Route, Router } from 'react-router'; import { Provider } from 'react-redux'; import { syncHistoryWithStore } from 'react-router-redux'; @@ -27,13 +27,14 @@ import Kolide404 from 'pages/Kolide404'; import Kolide500 from 'pages/Kolide500'; import store from 'redux/store'; import UserSettingsPage from 'pages/UserSettingsPage'; +import PATHS from 'router/paths'; const history = syncHistoryWithStore(browserHistory, store); const routes = ( - + @@ -45,7 +46,7 @@ const routes = ( - + @@ -75,7 +76,7 @@ const routes = ( - + ); diff --git a/frontend/router/paths.js b/frontend/router/paths.js index e77e96c91a..a45cf07c8d 100644 --- a/frontend/router/paths.js +++ b/frontend/router/paths.js @@ -1,19 +1,29 @@ +import URL_PREFIX from 'router/url_prefix'; + export default { - ADMIN_DASHBOARD: '/admin', - ADMIN_SETTINGS: '/admin/settings', - ALL_PACKS: '/packs/all', - EDIT_QUERY: (query) => { - return `/queries/${query.id}`; + ADMIN_USERS: `${URL_PREFIX}/admin/users`, + ADMIN_SETTINGS: `${URL_PREFIX}/admin/settings`, + ALL_PACKS: `${URL_PREFIX}/packs/all`, + EDIT_PACK: (pack) => { + return `${URL_PREFIX}/packs/${pack.id}/edit`; }, - FORGOT_PASSWORD: '/login/forgot', - HOME: '/', - KOLIDE_500: '/500', - LOGIN: '/login', - LOGOUT: '/logout', - MANAGE_HOSTS: '/hosts/manage', - NEW_PACK: '/packs/new', - NEW_QUERY: '/queries/new', - RESET_PASSWORD: '/login/reset', - SETUP: '/setup', - USER_SETTINGS: '/settings', + PACK: (pack) => { + return `${URL_PREFIX}/packs/${pack.id}`; + }, + EDIT_QUERY: (query) => { + return `${URL_PREFIX}/queries/${query.id}`; + }, + FORGOT_PASSWORD: `${URL_PREFIX}/login/forgot`, + HOME: `${URL_PREFIX}/`, + KOLIDE_500: `${URL_PREFIX}/500`, + LOGIN: `${URL_PREFIX}/login`, + LOGOUT: `${URL_PREFIX}/logout`, + MANAGE_HOSTS: `${URL_PREFIX}/hosts/manage`, + MANAGE_PACKS: `${URL_PREFIX}/packs/manage`, + NEW_PACK: `${URL_PREFIX}/packs/new`, + MANAGE_QUERIES: `${URL_PREFIX}/queries/manage`, + NEW_QUERY: `${URL_PREFIX}/queries/new`, + RESET_PASSWORD: `${URL_PREFIX}/login/reset`, + SETUP: `${URL_PREFIX}/setup`, + USER_SETTINGS: `${URL_PREFIX}/settings`, }; diff --git a/frontend/router/url_prefix.js b/frontend/router/url_prefix.js new file mode 100644 index 0000000000..1f17ef9ed6 --- /dev/null +++ b/frontend/router/url_prefix.js @@ -0,0 +1,4 @@ +// Encapsulate the URL Prefix so that this is the only module that +// needs to access the global. All other modules should use this one. + +export default window.urlPrefix || ''; diff --git a/frontend/styles/global/_fonts.scss b/frontend/styles/global/_fonts.scss index cde0521276..1e2f56c947 100644 --- a/frontend/styles/global/_fonts.scss +++ b/frontend/styles/global/_fonts.scss @@ -1,47 +1,47 @@ @font-face { font-family: 'Oxygen'; - src: url('/assets/fonts/oxygen/Oxygen-Light.eot'); - src: url('/assets/fonts/oxygen/Oxygen-Light.eot?#iefix') format('embedded-opentype'), - url('/assets/fonts/oxygen/Oxygen-Light.woff') format('woff'), - url('/assets/fonts/oxygen/Oxygen-Light.ttf') format('truetype'); + src: url('../assets/fonts/oxygen/Oxygen-Light.eot'); + src: url('../assets/fonts/oxygen/Oxygen-Light.eot?#iefix') format('embedded-opentype'), + url('../assets/fonts/oxygen/Oxygen-Light.woff') format('woff'), + url('../assets/fonts/oxygen/Oxygen-Light.ttf') format('truetype'); font-weight: 300; font-style: normal; } @font-face { font-family: 'Oxygen'; - src: url('/assets/fonts/oxygen/Oxygen-Bold.eot'); - src: url('/assets/fonts/oxygen/Oxygen-Bold.eot?#iefix') format('embedded-opentype'), - url('/assets/fonts/oxygen/Oxygen-Bold.woff') format('woff'), - url('/assets/fonts/oxygen/Oxygen-Bold.ttf') format('truetype'); + src: url('../assets/fonts/oxygen/Oxygen-Bold.eot'); + src: url('../assets/fonts/oxygen/Oxygen-Bold.eot?#iefix') format('embedded-opentype'), + url('../assets/fonts/oxygen/Oxygen-Bold.woff') format('woff'), + url('../assets/fonts/oxygen/Oxygen-Bold.ttf') format('truetype'); font-weight: bold; font-style: normal; } @font-face { font-family: 'Oxygen'; - src: url('/assets/fonts/oxygen/Oxygen-Regular.eot'); - src: url('/assets/fonts/oxygen/Oxygen-Regular.eot?#iefix') format('embedded-opentype'), - url('/assets/fonts/oxygen/Oxygen-Regular.woff') format('woff'), - url('/assets/fonts/oxygen/Oxygen-Regular.ttf') format('truetype'); + src: url('../assets/fonts/oxygen/Oxygen-Regular.eot'); + src: url('../assets/fonts/oxygen/Oxygen-Regular.eot?#iefix') format('embedded-opentype'), + url('../assets/fonts/oxygen/Oxygen-Regular.woff') format('woff'), + url('../assets/fonts/oxygen/Oxygen-Regular.ttf') format('truetype'); font-weight: normal; font-style: normal; } @font-face { font-family: 'kolidecons'; - src: url('/assets/fonts/kolidecons/kolidecons.woff2') format('woff2'), - url('/assets/fonts/kolidecons/kolidecons.woff') format('woff'); + src: url('../assets/fonts/kolidecons/kolidecons.woff2') format('woff2'), + url('../assets/fonts/kolidecons/kolidecons.woff') format('woff'); font-weight: normal; font-style: normal; } @font-face { font-family: 'SourceCodePro'; - src: url('/assets/fonts/source-code-pro/SourceCodePro-Regular.eot'); - src: url('/assets/fonts/source-code-pro/SourceCodePro-Regular.eot?#iefix') format('embedded-opentype'), - url('/assets/fonts/source-code-pro/SourceCodePro-Regular.ttf.woff') format('woff'), - url('/assets/fonts/source-code-pro/SourceCodePro-Regular.ttf') format('truetype'); + src: url('../assets/fonts/source-code-pro/SourceCodePro-Regular.eot'); + src: url('../assets/fonts/source-code-pro/SourceCodePro-Regular.eot?#iefix') format('embedded-opentype'), + url('../assets/fonts/source-code-pro/SourceCodePro-Regular.ttf.woff') format('woff'), + url('../assets/fonts/source-code-pro/SourceCodePro-Regular.ttf') format('truetype'); font-weight: normal; font-style: normal; } diff --git a/frontend/styles/global/_global.scss b/frontend/styles/global/_global.scss index df30a58df2..ecbffe1840 100644 --- a/frontend/styles/global/_global.scss +++ b/frontend/styles/global/_global.scss @@ -51,7 +51,7 @@ a { margin: 0; &--background { - background: url('/assets/images/background.png') center center; + background: url('../assets/images/background.png') center center; background-size: cover; } } diff --git a/frontend/templates/react.ejs b/frontend/templates/react.ejs index 4180a356f9..72ec664d05 100644 --- a/frontend/templates/react.ejs +++ b/frontend/templates/react.ejs @@ -1,24 +1,24 @@ - + - + - + - + - + - + - + @@ -28,43 +28,46 @@ - + - + - + - + - + - + - + - + - + - - - - - - - - - - + + + + + + + + + + - + Kolide Fleet +
- + diff --git a/server/config/config.go b/server/config/config.go index 56b55c287b..76ba17a1af 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -50,6 +50,7 @@ type ServerConfig struct { Key string TLS bool TLSProfile string + URLPrefix string `yaml:"url_prefix"` } // AuthConfig defines configs related to user authorization @@ -173,6 +174,8 @@ func (man Manager) addConfigs() { man.addConfigString(TLSProfileKey, TLSProfileModern, fmt.Sprintf("TLS security profile choose one of %s, %s or %s", TLSProfileModern, TLSProfileIntermediate, TLSProfileOld)) + man.addConfigString("server.url_prefix", "", + "URL prefix used on server and frontend endpoints") // Auth man.addConfigString("auth.jwt_key", "", @@ -272,6 +275,7 @@ func (man Manager) LoadConfig() KolideConfig { Key: man.getConfigString("server.key"), TLS: man.getConfigBool("server.tls"), TLSProfile: man.getConfigTLSProfile(), + URLPrefix: man.getConfigString("server.url_prefix"), }, Auth: AuthConfig{ JwtKey: man.getConfigString("auth.jwt_key"), diff --git a/server/datastore/datastore_options_test.go b/server/datastore/datastore_options_test.go index bc9c91cb73..62b9421622 100644 --- a/server/datastore/datastore_options_test.go +++ b/server/datastore/datastore_options_test.go @@ -90,8 +90,7 @@ func testOptionsToConfig(t *testing.T, ds kolide.Datastore) { require.Nil(t, ds.MigrateData()) resp, err := ds.GetOsqueryConfigOptions() require.Nil(t, err) - assert.Len(t, resp, 10) - assert.Equal(t, "/api/v1/osquery/distributed/read", resp["distributed_tls_read_endpoint"]) + assert.Len(t, resp, 8) opt, _ := ds.OptionByName("aws_profile_name") assert.False(t, opt.OptionSet()) opt.SetValue("zip") @@ -99,7 +98,7 @@ func testOptionsToConfig(t *testing.T, ds kolide.Datastore) { require.Nil(t, err) resp, err = ds.GetOsqueryConfigOptions() require.Nil(t, err) - assert.Len(t, resp, 11) + assert.Len(t, resp, 9) assert.Equal(t, "zip", resp["aws_profile_name"]) } diff --git a/server/datastore/internal/appstate/options.go b/server/datastore/internal/appstate/options.go index 1a11f057eb..85c88ceea9 100644 --- a/server/datastore/internal/appstate/options.go +++ b/server/datastore/internal/appstate/options.go @@ -21,8 +21,6 @@ func Options() []struct { // raise an error {"disable_distributed", false, kolide.OptionTypeBool, kolide.ReadOnly}, {"distributed_plugin", "tls", kolide.OptionTypeString, kolide.ReadOnly}, - {"distributed_tls_read_endpoint", "/api/v1/osquery/distributed/read", kolide.OptionTypeString, kolide.ReadOnly}, - {"distributed_tls_write_endpoint", "/api/v1/osquery/distributed/write", kolide.OptionTypeString, kolide.ReadOnly}, {"pack_delimiter", "/", kolide.OptionTypeString, kolide.ReadOnly}, // These options may be modified by an admin user {"aws_access_key_id", nil, kolide.OptionTypeString, kolide.NotReadOnly}, diff --git a/server/kolide/emails.go b/server/kolide/emails.go index 11616ef611..09379b65c8 100644 --- a/server/kolide/emails.go +++ b/server/kolide/emails.go @@ -49,8 +49,8 @@ type PasswordResetRequest struct { // SMTPTestMailer is used to build an email message that will be used as // a test message when testing SMTP configuration type SMTPTestMailer struct { - KolideServerURL template.URL - AssetURL template.URL + BaseURL template.URL + AssetURL template.URL } func (m *SMTPTestMailer) Message() ([]byte, error) { @@ -68,8 +68,8 @@ func (m *SMTPTestMailer) Message() ([]byte, error) { } type PasswordResetMailer struct { - // URL for the Fleet application - KolideServerURL template.URL + // Base URL to use for Fleet endpoints + BaseURL template.URL // URL for loading image assets AssetURL template.URL // Token password reset token diff --git a/server/kolide/emails_test.go b/server/kolide/emails_test.go index 98b36c4453..a4c0b3e124 100644 --- a/server/kolide/emails_test.go +++ b/server/kolide/emails_test.go @@ -9,8 +9,8 @@ import ( func TestTemplateProcessor(t *testing.T) { mailer := PasswordResetMailer{ - KolideServerURL: "https://localhost.com:8080", - Token: "12345", + BaseURL: "https://localhost.com:8080", + Token: "12345", } out, err := mailer.Message() diff --git a/server/kolide/invites.go b/server/kolide/invites.go index 29aae4fa9f..618549d85a 100644 --- a/server/kolide/invites.go +++ b/server/kolide/invites.go @@ -75,7 +75,7 @@ type Invite struct { // InviteMailer is used to build an email template for the invite email. type InviteMailer struct { *Invite - KolideServerURL template.URL + BaseURL template.URL AssetURL template.URL InvitedByUsername string OrgName string diff --git a/server/kolide/users.go b/server/kolide/users.go index 8d8a3cf020..69d5b8b82b 100644 --- a/server/kolide/users.go +++ b/server/kolide/users.go @@ -194,9 +194,9 @@ func falseIfNil(b *bool) bool { } type ChangeEmailMailer struct { - KolideServerURL template.URL - AssetURL template.URL - Token string + BaseURL template.URL + AssetURL template.URL + Token string } func (cem *ChangeEmailMailer) Message() ([]byte, error) { diff --git a/server/mail/mail_test.go b/server/mail/mail_test.go index 5293e62a28..e056589844 100644 --- a/server/mail/mail_test.go +++ b/server/mail/mail_test.go @@ -66,7 +66,7 @@ func testSMTPPlainAuth(t *testing.T, mailer kolide.MailService) { SMTPSenderAddress: "kolide@kolide.com", }, Mailer: &kolide.SMTPTestMailer{ - KolideServerURL: "https://localhost:8080", + BaseURL: "https://localhost:8080", }, } @@ -92,7 +92,7 @@ func testSMTPSkipVerify(t *testing.T, mailer kolide.MailService) { SMTPSenderAddress: "kolide@kolide.com", }, Mailer: &kolide.SMTPTestMailer{ - KolideServerURL: "https://localhost:8080", + BaseURL: "https://localhost:8080", }, } @@ -114,7 +114,7 @@ func testSMTPNoAuth(t *testing.T, mailer kolide.MailService) { SMTPSenderAddress: "kolide@kolide.com", }, Mailer: &kolide.SMTPTestMailer{ - KolideServerURL: "https://localhost:8080", + BaseURL: "https://localhost:8080", }, } @@ -136,7 +136,7 @@ func testMailTest(t *testing.T, mailer kolide.MailService) { SMTPSenderAddress: "kolide@kolide.com", }, Mailer: &kolide.SMTPTestMailer{ - KolideServerURL: "https://localhost:8080", + BaseURL: "https://localhost:8080", }, } err := Test(mailer, mail) diff --git a/server/mail/templates/change_email_confirmation.html b/server/mail/templates/change_email_confirmation.html index eda8583912..36f5cdff09 100644 --- a/server/mail/templates/change_email_confirmation.html +++ b/server/mail/templates/change_email_confirmation.html @@ -58,7 +58,7 @@
- {{.KolideServerURL}}/email/change/{{.Token}} + {{.BaseURL}}/email/change/{{.Token}}
diff --git a/server/mail/templates/invite_token.html b/server/mail/templates/invite_token.html index 925abd109f..79d384ebf6 100644 --- a/server/mail/templates/invite_token.html +++ b/server/mail/templates/invite_token.html @@ -59,9 +59,9 @@ {{if .SSOEnabled}} - {{.KolideServerURL}}/login/ssoinvites/{{.Token}}?name={{.Name}}&email={{.Email}} + {{.BaseURL}}/login/ssoinvites/{{.Token}}?name={{.Name}}&email={{.Email}} {{else}} - {{.KolideServerURL}}/login/invites/{{.Token}}?name={{.Name}}&email={{.Email}} + {{.BaseURL}}/login/invites/{{.Token}}?name={{.Name}}&email={{.Email}} {{end}} diff --git a/server/mail/templates/password_reset.html b/server/mail/templates/password_reset.html index 9764e77d56..4ac40a1b92 100644 --- a/server/mail/templates/password_reset.html +++ b/server/mail/templates/password_reset.html @@ -54,7 +54,7 @@

Reset Your Fleet Password...

Someone requested a password reset on your Fleet account. Follow the link below to reset your password:

-

Reset Password

+

Reset Password

If you did not make the request, you may ignore this email as no changes have been made.

diff --git a/server/mail/templates/smtp_setup.html b/server/mail/templates/smtp_setup.html index 48219a7778..04a73c7dfa 100644 --- a/server/mail/templates/smtp_setup.html +++ b/server/mail/templates/smtp_setup.html @@ -53,7 +53,7 @@

Confirmed Fleet SMTP Setup

-

This message confirms that SMTP is set up properly on your Fleet instance.

+

This message confirms that SMTP is set up properly on your Fleet instance.

diff --git a/server/service/client.go b/server/service/client.go index 3dbac6fa26..b82b0e9a79 100644 --- a/server/service/client.go +++ b/server/service/client.go @@ -17,12 +17,13 @@ import ( type Client struct { addr string baseURL *url.URL + urlPrefix string token string http *http.Client insecureSkipVerify bool } -func NewClient(addr string, insecureSkipVerify bool, rootCA string) (*Client, error) { +func NewClient(addr string, insecureSkipVerify bool, rootCA, urlPrefix string) (*Client, error) { if !strings.HasPrefix(addr, "https://") { return nil, errors.New("Address must start with https://") } @@ -67,6 +68,7 @@ func NewClient(addr string, insecureSkipVerify bool, rootCA string) (*Client, er baseURL: baseURL, http: httpClient, insecureSkipVerify: insecureSkipVerify, + urlPrefix: urlPrefix, }, nil } @@ -124,6 +126,6 @@ func (c *Client) SetToken(t string) { func (c *Client) url(path string) *url.URL { u := *c.baseURL - u.Path = path + u.Path = c.urlPrefix + path return &u } diff --git a/server/service/client_live_query.go b/server/service/client_live_query.go index 5a070516f7..854195a348 100644 --- a/server/service/client_live_query.go +++ b/server/service/client_live_query.go @@ -96,7 +96,7 @@ func (c *Client) LiveQuery(query string, labels []string, hosts []string) (*Live wssURL := *c.baseURL wssURL.Scheme = "wss" - wssURL.Path = "/api/v1/kolide/results/websocket" + wssURL.Path = c.urlPrefix + "/api/v1/kolide/results/websocket" conn, _, err := dialer.Dial(wssURL.String(), nil) if err != nil { return nil, errors.Wrap(err, "upgrade live query result websocket") diff --git a/server/service/endpoint_sessions.go b/server/service/endpoint_sessions.go index e02ad93f48..47f7c5313e 100644 --- a/server/service/endpoint_sessions.go +++ b/server/service/endpoint_sessions.go @@ -207,7 +207,7 @@ func (r callbackSSOResponse) error() error { return r.Err } // If html is present we return a web page func (r callbackSSOResponse) html() string { return r.content } -func makeCallbackSSOEndpoint(svc kolide.Service) endpoint.Endpoint { +func makeCallbackSSOEndpoint(svc kolide.Service, urlPrefix string) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { authResponse := request.(kolide.Auth) session, err := svc.CallbackSSO(ctx, authResponse) @@ -216,7 +216,7 @@ func makeCallbackSSOEndpoint(svc kolide.Service) endpoint.Endpoint { // redirect to login page on front end if there was some problem, // errors should still be logged session = &kolide.SSOSession{ - RedirectURL: "/login", + RedirectURL: urlPrefix + "/login", Token: "", } resp.Err = err diff --git a/server/service/endpoint_test.go b/server/service/endpoint_test.go index 7784c9756a..ef48f5f630 100644 --- a/server/service/endpoint_test.go +++ b/server/service/endpoint_test.go @@ -57,7 +57,7 @@ func setupEndpointTest(t *testing.T) *testResource { logger := kitlog.NewLogfmtLogger(os.Stdout) jwtKey := "CHANGEME" - routes := MakeHandler(svc, jwtKey, logger) + routes := MakeHandler(svc, config.KolideConfig{Auth: config.AuthConfig{JwtKey: jwtKey}}, logger) test.server = httptest.NewServer(routes) diff --git a/server/service/frontend.go b/server/service/frontend.go index 0802b2e049..883972dda6 100644 --- a/server/service/frontend.go +++ b/server/service/frontend.go @@ -18,7 +18,7 @@ func newBinaryFileSystem(root string) *assetfs.AssetFS { } } -func ServeFrontend(logger log.Logger) http.Handler { +func ServeFrontend(urlPrefix string, logger log.Logger) http.Handler { herr := func(w http.ResponseWriter, err string) { logger.Log("err", err) http.Error(w, err, http.StatusInternalServerError) @@ -40,7 +40,7 @@ func ServeFrontend(logger log.Logger) http.Handler { herr(w, "create react template: "+err.Error()) return } - if err := t.Execute(w, nil); err != nil { + if err := t.Execute(w, struct{ URLPrefix string }{urlPrefix}); err != nil { herr(w, "execute react template: "+err.Error()) return } diff --git a/server/service/handler.go b/server/service/handler.go index b13e58475c..fdded503b5 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -9,6 +9,7 @@ import ( kitlog "github.com/go-kit/kit/log" kithttp "github.com/go-kit/kit/transport/http" "github.com/gorilla/mux" + "github.com/kolide/fleet/server/config" "github.com/kolide/fleet/server/kolide" "github.com/prometheus/client_golang/prometheus" ) @@ -100,7 +101,7 @@ type KolideEndpoints struct { } // MakeKolideServerEndpoints creates the Kolide API endpoints. -func MakeKolideServerEndpoints(svc kolide.Service, jwtKey string) KolideEndpoints { +func MakeKolideServerEndpoints(svc kolide.Service, jwtKey, urlPrefix string) KolideEndpoints { return KolideEndpoints{ Login: makeLoginEndpoint(svc), Logout: makeLogoutEndpoint(svc), @@ -109,7 +110,7 @@ func MakeKolideServerEndpoints(svc kolide.Service, jwtKey string) KolideEndpoint CreateUser: makeCreateUserEndpoint(svc), VerifyInvite: makeVerifyInviteEndpoint(svc), InitiateSSO: makeInitiateSSOEndpoint(svc), - CallbackSSO: makeCallbackSSOEndpoint(svc), + CallbackSSO: makeCallbackSSOEndpoint(svc, urlPrefix), SSOSettings: makeSSOSettingsEndpoint(svc), // Authenticated user endpoints @@ -377,11 +378,11 @@ func makeKolideKitHandlers(e KolideEndpoints, opts []kithttp.ServerOption) *koli } // MakeHandler creates an HTTP handler for the Fleet server endpoints. -func MakeHandler(svc kolide.Service, jwtKey string, logger kitlog.Logger) http.Handler { +func MakeHandler(svc kolide.Service, config config.KolideConfig, logger kitlog.Logger) http.Handler { kolideAPIOptions := []kithttp.ServerOption{ kithttp.ServerBefore( kithttp.PopulateRequestContext, // populate the request context with common fields - setRequestsContexts(svc, jwtKey), + setRequestsContexts(svc, config.Auth.JwtKey), ), kithttp.ServerErrorLogger(logger), kithttp.ServerErrorEncoder(encodeError), @@ -390,7 +391,7 @@ func MakeHandler(svc kolide.Service, jwtKey string, logger kitlog.Logger) http.H ), } - kolideEndpoints := MakeKolideServerEndpoints(svc, jwtKey) + kolideEndpoints := MakeKolideServerEndpoints(svc, config.Auth.JwtKey, config.Server.URLPrefix) kolideHandlers := makeKolideKitHandlers(kolideEndpoints, kolideAPIOptions) r := mux.NewRouter() @@ -398,7 +399,7 @@ func MakeHandler(svc kolide.Service, jwtKey string, logger kitlog.Logger) http.H addMetrics(r) r.PathPrefix("/api/v1/kolide/results/"). - Handler(makeStreamDistributedQueryCampaignResultsHandler(svc, jwtKey, logger)). + Handler(makeStreamDistributedQueryCampaignResultsHandler(svc, config.Auth.JwtKey, logger)). Name("distributed_query_results") return r @@ -543,7 +544,7 @@ func WithSetup(svc kolide.Service, logger kitlog.Logger, next http.Handler) http // RedirectLoginToSetup detects if the setup endpoint should be used. If setup is required it redirect all // frontend urls to /setup, otherwise the frontend router is used. -func RedirectLoginToSetup(svc kolide.Service, logger kitlog.Logger, next http.Handler) http.HandlerFunc { +func RedirectLoginToSetup(svc kolide.Service, logger kitlog.Logger, next http.Handler, urlPrefix string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { redirect := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/setup" { @@ -551,7 +552,7 @@ func RedirectLoginToSetup(svc kolide.Service, logger kitlog.Logger, next http.Ha return } newURL := r.URL - newURL.Path = "/setup" + newURL.Path = urlPrefix + "/setup" http.Redirect(w, r, newURL.String(), http.StatusTemporaryRedirect) }) @@ -565,7 +566,7 @@ func RedirectLoginToSetup(svc kolide.Service, logger kitlog.Logger, next http.Ha redirect.ServeHTTP(w, r) return } - RedirectSetupToLogin(svc, logger, next).ServeHTTP(w, r) + RedirectSetupToLogin(svc, logger, next, urlPrefix).ServeHTTP(w, r) } } @@ -584,11 +585,11 @@ func RequireSetup(svc kolide.Service) (bool, error) { // RedirectSetupToLogin forces the /setup path to be redirected to login. This middleware is used after // the app has been setup. -func RedirectSetupToLogin(svc kolide.Service, logger kitlog.Logger, next http.Handler) http.HandlerFunc { +func RedirectSetupToLogin(svc kolide.Service, logger kitlog.Logger, next http.Handler, urlPrefix string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/setup" { newURL := r.URL - newURL.Path = "/login" + newURL.Path = urlPrefix + "/login" http.Redirect(w, r, newURL.String(), http.StatusTemporaryRedirect) return } diff --git a/server/service/handler_test.go b/server/service/handler_test.go index 0ffbc7cf9d..4902597940 100644 --- a/server/service/handler_test.go +++ b/server/service/handler_test.go @@ -25,7 +25,7 @@ func TestAPIRoutes(t *testing.T) { assert.Nil(t, err) r := mux.NewRouter() - ke := MakeKolideServerEndpoints(svc, "CHANGEME") + ke := MakeKolideServerEndpoints(svc, "CHANGEME", "") kh := makeKolideKitHandlers(ke, nil) attachKolideAPIRoutes(r, kh) handler := mux.NewRouter() @@ -242,7 +242,7 @@ func TestModifyUserPermissions(t *testing.T) { svc, err := newTestService(ms, nil) assert.Nil(t, err) - handler := MakeHandler(svc, "CHANGEME", log.NewNopLogger()) + handler := MakeHandler(svc, config.KolideConfig{Auth: config.AuthConfig{JwtKey: "CHANGEME"}}, log.NewNopLogger()) testCases := []struct { ActingUserID uint diff --git a/server/service/http_auth_test.go b/server/service/http_auth_test.go index fc7f5916fb..9a4900775b 100644 --- a/server/service/http_auth_test.go +++ b/server/service/http_auth_test.go @@ -38,7 +38,7 @@ func TestLogin(t *testing.T) { ), } r := mux.NewRouter() - ke := MakeKolideServerEndpoints(svc, "CHANGEME") + ke := MakeKolideServerEndpoints(svc, "CHANGEME", "") kh := makeKolideKitHandlers(ke, opts) attachKolideAPIRoutes(r, kh) r.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/server/service/service_appconfig.go b/server/service/service_appconfig.go index c085c5787f..e5def21f5f 100644 --- a/server/service/service_appconfig.go +++ b/server/service/service_appconfig.go @@ -65,8 +65,8 @@ func (svc service) SendTestEmail(ctx context.Context, config *kolide.AppConfig) Subject: "Hello from Fleet", To: []string{vc.User.Email}, Mailer: &kolide.SMTPTestMailer{ - KolideServerURL: template.URL(config.KolideServerURL), - AssetURL: getAssetURL(), + BaseURL: template.URL(config.KolideServerURL + svc.config.Server.URLPrefix), + AssetURL: getAssetURL(), }, Config: config, } diff --git a/server/service/service_invites.go b/server/service/service_invites.go index ca46b6b98e..53447911a1 100644 --- a/server/service/service_invites.go +++ b/server/service/service_invites.go @@ -67,7 +67,7 @@ func (svc service) InviteNewUser(ctx context.Context, payload kolide.InvitePaylo Config: config, Mailer: &kolide.InviteMailer{ Invite: invite, - KolideServerURL: template.URL(config.KolideServerURL), + BaseURL: template.URL(config.KolideServerURL + svc.config.Server.URLPrefix), AssetURL: getAssetURL(), OrgName: config.OrgName, InvitedByUsername: invitedBy, diff --git a/server/service/service_sessions.go b/server/service/service_sessions.go index 36e60ac41d..e8e1357aaa 100644 --- a/server/service/service_sessions.go +++ b/server/service/service_sessions.go @@ -42,7 +42,7 @@ func (svc service) InitiateSSO(ctx context.Context, redirectURL string) (string, settings := sso.Settings{ Metadata: metadata, // Construct call back url to send to idp - AssertionConsumerServiceURL: appConfig.KolideServerURL + "/api/v1/kolide/sso/callback", + AssertionConsumerServiceURL: appConfig.KolideServerURL + svc.config.Server.URLPrefix + "/api/v1/kolide/sso/callback", SessionStore: svc.ssoSessionStore, OriginalURL: redirectURL, } @@ -117,7 +117,7 @@ func (svc service) CallbackSSO(ctx context.Context, auth kolide.Auth) (*kolide.S RedirectURL: sess.OriginalURL, } if !strings.HasPrefix(result.RedirectURL, "/") { - result.RedirectURL = "/" + result.RedirectURL + result.RedirectURL = svc.config.Server.URLPrefix + result.RedirectURL } return result, nil } diff --git a/server/service/service_users.go b/server/service/service_users.go index 3ad2d6e95f..3447db7ef6 100644 --- a/server/service/service_users.go +++ b/server/service/service_users.go @@ -153,9 +153,9 @@ func (svc service) modifyEmailAddress(ctx context.Context, user *kolide.User, em To: []string{email}, Config: config, Mailer: &kolide.ChangeEmailMailer{ - Token: token, - KolideServerURL: template.URL(config.KolideServerURL), - AssetURL: getAssetURL(), + Token: token, + BaseURL: template.URL(config.KolideServerURL + svc.config.Server.URLPrefix), + AssetURL: getAssetURL(), }, } return svc.mailService.SendEmail(changeEmail) @@ -355,9 +355,9 @@ func (svc service) RequestPasswordReset(ctx context.Context, email string) error To: []string{user.Email}, Config: config, Mailer: &kolide.PasswordResetMailer{ - KolideServerURL: template.URL(config.KolideServerURL), - AssetURL: getAssetURL(), - Token: token, + BaseURL: template.URL(config.KolideServerURL + svc.config.Server.URLPrefix), + AssetURL: getAssetURL(), + Token: token, }, } diff --git a/webpack.config.js b/webpack.config.js index 64e916b71b..9744534533 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -56,7 +56,16 @@ var config = { noParse: /node_modules\/sqlite-parser\/dist\/sqlite-parser-min.js/, rules: [ { test: /\.(png|gif)$/, use: { loader: 'url-loader?name=[name]@[hash].[ext]&limit=6000' } }, - { test: /\.(pdf|ico|jpg|svg|eot|otf|woff|ttf|mp4|webm)$/, use: { loader: 'file-loader?name=[name]@[hash].[ext]' } }, + { + test: /\.(pdf|ico|jpg|svg|eot|otf|woff|woff2|ttf|mp4|webm)$/, + use: { + loader: 'file-loader', + options: { + name: '[name]@[hash].[ext]', + useRelativePath: true, + }, + }, + }, { test: /\.tsx?$/, exclude: /node_modules/, use: { loader: 'ts-loader' } }, { test: /\.scss$/, @@ -65,6 +74,7 @@ var config = { { loader: MiniCssExtractPlugin.loader, options: { + publicPath: './', hmr: process.env.NODE_ENV == 'development', }, },