Add ability to prefix Fleet URLs (#2112)

- Add the server_url_prefix flag for configuring this functionality
- Add prefix handling to the server routes
- Refactor JS to use appropriate paths from modules
- Use JS template to get URL prefix into JS environment
- Update webpack config to support prefixing

Thanks to securityonion.net for sponsoring the development of this feature.

Closes #1661
This commit is contained in:
Zachary Wasserman 2019-10-16 16:40:45 -07:00 committed by GitHub
parent 59efb495ca
commit adf87140a7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
52 changed files with 312 additions and 199 deletions

6
.gitignore vendored
View file

@ -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/
mysqldata/

View file

@ -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,

View file

@ -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")
}

View file

@ -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")
}

View file

@ -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
```

View file

@ -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 (
<div className={baseClass}>
<img alt="Kolide Fleet" src="/assets/images/kolide-logo-vertical.svg" className={`${baseClass}__logo`} />
<img alt="Kolide Fleet" src={logoVertical} className={`${baseClass}__logo`} />
{children}
</div>
);

View file

@ -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%;

View file

@ -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,

View file

@ -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;

View file

@ -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 {
<nav className={`${toggleBaseClass}__nav`}>
<ul className={`${toggleBaseClass}__nav-list`}>
<li className={`${toggleBaseClass}__nav-item`}><a href="#settings" onClick={onNavItemClick('/settings')}><Icon name="user-settings" /><span>Account Settings</span></a></li>
<li className={`${toggleBaseClass}__nav-item`}><a href="#settings" onClick={onNavItemClick(PATHS.USER_SETTINGS)}><Icon name="user-settings" /><span>Account Settings</span></a></li>
<li className={`${toggleBaseClass}__nav-item`}><a href="#logout" onClick={onLogout}><Icon name="logout" /><span>Log Out</span></a></li>
</ul>
</nav>

View file

@ -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,
},
},
],

View file

@ -1,5 +1,6 @@
import ReactDOM from 'react-dom';
import './public-path';
import routes from './router';
import './index.scss';

View file

@ -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');
}

View file

@ -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);
}

View file

@ -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,

View file

@ -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;
}

View file

@ -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) => {

View file

@ -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;
}

View file

@ -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);

View file

@ -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);
})

4
frontend/public-path.js Normal file
View file

@ -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

View file

@ -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 = (
<Provider store={store}>
<Router history={history}>
<Route path="/" component={App}>
<Route path={PATHS.HOME} component={App}>
<Route path="setup" component={RegistrationPage} />
<Route path="login" component={LoginRoutes}>
<Route path="invites/:invite_token" component={ConfirmInvitePage} />
@ -45,7 +46,7 @@ const routes = (
<Route path="email/change/:token" component={EmailTokenRedirect} />
<Route path="logout" component={LogoutPage} />
<Route component={CoreLayout}>
<IndexRedirect to="/hosts/manage" />
<IndexRedirect to={PATHS.MANAGE_HOSTS} />
<Route path="admin" component={AuthenticatedAdminRoutes}>
<Route path="users" component={AdminUserManagementPage} />
<Route path="settings" component={AdminAppSettingsPage} />
@ -75,7 +76,7 @@ const routes = (
</Route>
<Route path="/500" component={Kolide500} />
<Route path="/404" component={Kolide404} />
<Redirect from="*" to="/404" />
<Route component={Kolide404} />
</Router>
</Provider>
);

View file

@ -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`,
};

View file

@ -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 || '';

View file

@ -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; }

View file

@ -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;
}
}

View file

@ -1,24 +1,24 @@
<!DOCTYPE html>
<html data-uuid="{{ .UUID }}">
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="<%= htmlWebpackPlugin.files.css[0] %>">
<link rel="stylesheet" type="text/css" href="{{.URLPrefix}}<%= htmlWebpackPlugin.files.css[0] %>">
<!-- Icons & Platform Specific Settings - Favicon generator used to generate the icons below http://realfavicongenerator.net -->
<!-- shortcut icon - This file contains the following sizes 16x16, 32x32 and 48x48. -->
<link rel="shortcut icon" href="/assets/favicons/favicon.ico">
<link rel="shortcut icon" href="{{.URLPrefix}}/assets/favicons/favicon.ico">
<!-- favicon-96x96.png - For Google TV https://developer.android.com/training/tv/index.html#favicons. -->
<link rel="icon" type="image/png" href="/assets/favicons/favicon-96x96.png" sizes="96x96">
<link rel="icon" type="image/png" href="{{.URLPrefix}}/assets/favicons/favicon-96x96.png" sizes="96x96">
<!-- favicon-32x32.png - For Safari on Mac OS. -->
<link rel="icon" type="image/png" href="/assets/favicons/favicon-32x32.png" sizes="32x32">
<link rel="icon" type="image/png" href="{{.URLPrefix}}/assets/favicons/favicon-32x32.png" sizes="32x32">
<!-- favicon-16x16.png - The classic favicon, displayed in the tabs. -->
<link rel="icon" type="image/png" href="/assets/favicons/favicon-16x16.png" sizes="16x16">
<link rel="icon" type="image/png" href="{{.URLPrefix}}/assets/favicons/favicon-16x16.png" sizes="16x16">
<!-- Android/Chrome -->
<!-- manifest-json - The location of the browser configuration file. It contains locations of icon files, name of the application and default device screen orientation. Note that the name field is mandatory.
https://developer.chrome.com/multidevice/android/installtohomescreen. -->
<link rel="manifest" href="/assets/favicons/manifest.json">
<link rel="manifest" href="{{.URLPrefix}}/assets/favicons/manifest.json">
<!-- theme-color - The colour of the toolbar in Chrome M39+
http://updates.html5rocks.com/2014/11/Support-for-theme-color-in-Chrome-39-for-Android -->
<meta name="theme-color" content="#1E1E1E">
@ -28,43 +28,46 @@
<!-- Apple Icons - You can move all these icons to the root of the site and remove these link elements, if you don't mind the clutter.
https://developer.apple.com/library/safari/documentation/AppleApplications/Reference/SafariHTMLRef/Introduction.html#//apple_ref/doc/uid/30001261-SW1 -->
<!-- apple-touch-icon-57x57.png - Android Stock Browser and non-Retina iPhone and iPod Touch -->
<link rel="apple-touch-icon" sizes="57x57" href="/assets/favicons/apple-touch-icon-57x57.png">
<link rel="apple-touch-icon" sizes="57x57" href="{{.URLPrefix}}/assets/favicons/apple-touch-icon-57x57.png">
<!-- apple-touch-icon-114x114.png - iPhone (with 2× display) iOS = 6 -->
<link rel="apple-touch-icon" sizes="114x114" href="/assets/favicons/apple-touch-icon-114x114.png">
<link rel="apple-touch-icon" sizes="114x114" href="{{.URLPrefix}}/assets/favicons/apple-touch-icon-114x114.png">
<!-- apple-touch-icon-72x72.png - iPad mini and the first- and second-generation iPad (1× display) on iOS = 6 -->
<link rel="apple-touch-icon" sizes="72x72" href="/assets/favicons/apple-touch-icon-72x72.png">
<link rel="apple-touch-icon" sizes="72x72" href="{{.URLPrefix}}/assets/favicons/apple-touch-icon-72x72.png">
<!-- apple-touch-icon-144x144.png - iPad (with 2× display) iOS = 6 -->
<link rel="apple-touch-icon" sizes="144x144" href="/assets/favicons/apple-touch-icon-144x144.png">
<link rel="apple-touch-icon" sizes="144x144" href="{{.URLPrefix}}/assets/favicons/apple-touch-icon-144x144.png">
<!-- apple-touch-icon-60x60.png - Same as apple-touch-icon-57x57.png, for non-retina iPhone with iOS7. -->
<link rel="apple-touch-icon" sizes="60x60" href="/assets/favicons/apple-touch-icon-60x60.png">
<link rel="apple-touch-icon" sizes="60x60" href="{{.URLPrefix}}/assets/favicons/apple-touch-icon-60x60.png">
<!-- apple-touch-icon-120x120.png - iPhone (with 2× and 3 display) iOS = 7 -->
<link rel="apple-touch-icon" sizes="120x120" href="/assets/favicons/apple-touch-icon-120x120.png">
<link rel="apple-touch-icon" sizes="120x120" href="{{.URLPrefix}}/assets/favicons/apple-touch-icon-120x120.png">
<!-- apple-touch-icon-76x76.png - iPad mini and the first- and second-generation iPad (1× display) on iOS = 7 -->
<link rel="apple-touch-icon" sizes="76x76" href="/assets/favicons/apple-touch-icon-76x76.png">
<link rel="apple-touch-icon" sizes="76x76" href="{{.URLPrefix}}/assets/favicons/apple-touch-icon-76x76.png">
<!-- apple-touch-icon-152x152.png - iPad 3+ (with 2× display) iOS = 7 -->
<link rel="apple-touch-icon" sizes="152x152" href="/assets/favicons/apple-touch-icon-152x152.png">
<link rel="apple-touch-icon" sizes="152x152" href="{{.URLPrefix}}/assets/favicons/apple-touch-icon-152x152.png">
<!-- apple-touch-icon-180x180.png - iPad and iPad mini (with 2× display) iOS = 8 -->
<link rel="apple-touch-icon" sizes="180x180" href="/assets/favicons/apple-touch-icon-180x180.png">
<link rel="apple-touch-icon" sizes="180x180" href="{{.URLPrefix}}/assets/favicons/apple-touch-icon-180x180.png">
<link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 480px) and (-webkit-device-pixel-ratio: 1)" href="/assets/favicons/apple-touch-startup-image-320x460.png"/>
<link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 480px) and (-webkit-device-pixel-ratio: 2)" href="/assets/favicons/apple-touch-startup-image-640x920.png"/>
<link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2)" href="/assets/favicons/apple-touch-startup-image-640x1096.png"/>
<link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2)" href="/assets/favicons/apple-touch-startup-image-750x1294.png"/>
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (orientation: landscape) and (-webkit-device-pixel-ratio: 3)" href="/assets/favicons/apple-touch-startup-image-1182x2208.png"/>
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 3)" href="/assets/favicons/apple-touch-startup-image-1242x2148.png"/>
<link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (orientation: landscape) and (-webkit-device-pixel-ratio: 1)" href="/assets/favicons/apple-touch-startup-image-748x1024.png"/>
<link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 1)" href="/assets/favicons/apple-touch-startup-image-768x1004.png"/>
<link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (orientation: landscape) and (-webkit-device-pixel-ratio: 2)" href="/assets/favicons/apple-touch-startup-image-1496x2048.png"/>
<link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 2)" href="/assets/favicons/apple-touch-startup-image-1536x2008.png"/>
<link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 480px) and (-webkit-device-pixel-ratio: 1)" href="{{.URLPrefix}}/assets/favicons/apple-touch-startup-image-320x460.png"/>
<link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 480px) and (-webkit-device-pixel-ratio: 2)" href="{{.URLPrefix}}/assets/favicons/apple-touch-startup-image-640x920.png"/>
<link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2)" href="{{.URLPrefix}}/assets/favicons/apple-touch-startup-image-640x1096.png"/>
<link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2)" href="{{.URLPrefix}}/assets/favicons/apple-touch-startup-image-750x1294.png"/>
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (orientation: landscape) and (-webkit-device-pixel-ratio: 3)" href="{{.URLPrefix}}/assets/favicons/apple-touch-startup-image-1182x2208.png"/>
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 3)" href="{{.URLPrefix}}/assets/favicons/apple-touch-startup-image-1242x2148.png"/>
<link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (orientation: landscape) and (-webkit-device-pixel-ratio: 1)" href="{{.URLPrefix}}/assets/favicons/apple-touch-startup-image-748x1024.png"/>
<link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 1)" href="{{.URLPrefix}}/assets/favicons/apple-touch-startup-image-768x1004.png"/>
<link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (orientation: landscape) and (-webkit-device-pixel-ratio: 2)" href="{{.URLPrefix}}/assets/favicons/apple-touch-startup-image-1496x2048.png"/>
<link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 2)" href="{{.URLPrefix}}/assets/favicons/apple-touch-startup-image-1536x2008.png"/>
<!-- Windows 8.1 IE11 -->
<!-- msapplication-config - The location of the browser configuration file. If you have an RSS feed, go to
http://www.buildmypinnedsite.com and regenerate the browserconfig.xml file. You will then have a cool live tile! -->
<meta name="msapplication-config" content="/assets/favicons/browserconfig.xml">
<meta name="msapplication-config" content="{{.URLPrefix}}/assets/favicons/browserconfig.xml">
<title>Kolide Fleet</title>
<script type="text/javascript">
var urlPrefix = "{{.URLPrefix}}";
</script>
</head>
<body>
<div id="app"></div>
<script async defer src="<%= htmlWebpackPlugin.files.js[0] %>" onload="this.parentElement.removeChild(this)"></script>
<script async defer src="{{.URLPrefix}}<%= htmlWebpackPlugin.files.js[0] %>" onload="this.parentElement.removeChild(this)"></script>
<!-- Because iOS hates interactive stuff, we have to kill it with fire -->
<script>document.addEventListener("touchstart", function() {},false);</script>
<!-- End Apple Hate -->

View file

@ -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"),

View file

@ -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"])
}

View file

@ -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},

View file

@ -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

View file

@ -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()

View file

@ -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

View file

@ -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) {

View file

@ -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)

View file

@ -58,7 +58,7 @@
<table bgcolor="#f4f6fb" height="100px" cellpadding="20px">
<tr>
<td style="font-family: 'Oxygen', Arial, sans-serif;">
<a href="{{.KolideServerURL}}/email/change/{{.Token}}">{{.KolideServerURL}}/email/change/{{.Token}}</a>
<a href="{{.BaseURL}}/email/change/{{.Token}}">{{.BaseURL}}/email/change/{{.Token}}</a>
</td>
</tr>
</table>

View file

@ -59,9 +59,9 @@
<tr>
<td style="font-family: 'Oxygen', Arial, sans-serif;">
{{if .SSOEnabled}}
<a href="{{.KolideServerURL}}/login/ssoinvites/{{.Token}}?name={{.Name}}&email={{.Email}}">{{.KolideServerURL}}/login/ssoinvites/{{.Token}}?name={{.Name}}&email={{.Email}}</a>
<a href="{{.BaseURL}}/login/ssoinvites/{{.Token}}?name={{.Name}}&email={{.Email}}">{{.BaseURL}}/login/ssoinvites/{{.Token}}?name={{.Name}}&email={{.Email}}</a>
{{else}}
<a href="{{.KolideServerURL}}/login/invites/{{.Token}}?name={{.Name}}&email={{.Email}}">{{.KolideServerURL}}/login/invites/{{.Token}}?name={{.Name}}&email={{.Email}}</a>
<a href="{{.BaseURL}}/login/invites/{{.Token}}?name={{.Name}}&email={{.Email}}">{{.BaseURL}}/login/invites/{{.Token}}?name={{.Name}}&email={{.Email}}</a>
{{end}}
</td>
</tr>

View file

@ -54,7 +54,7 @@
<td colspan="2" style="padding:60px; font-family: 'Oxygen', Arial, sans-serif;">
<h1 style="font-weight:300">Reset Your Fleet Password...</h1>
<p>Someone requested a password reset on your Fleet account. Follow the link below to reset your password:</p>
<p><a href="{{.KolideServerURL}}/login/reset?token={{.Token}}">Reset Password</a></p>
<p><a href="{{.BaseURL}}/login/reset?token={{.Token}}">Reset Password</a></p>
<p style="color:#9ca3ac"><em>If you did not make the request, you may ignore this email as no changes have been made.</em></p>
</td>
</tr>

View file

@ -53,7 +53,7 @@
<tr>
<td colspan="2" style="padding:60px; font-family: 'Oxygen', Arial, sans-serif;">
<h1 style="font-weight:300">Confirmed Fleet SMTP Setup</h1>
<p>This message confirms that SMTP is set up properly on your <a href="{{.KolideServerURL}}">Fleet instance</a>.</p>
<p>This message confirms that SMTP is set up properly on your <a href="{{.BaseURL}}">Fleet instance</a>.</p>
</td>
</tr>
<tr bgcolor="#9ca3ac">

View file

@ -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
}

View file

@ -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")

View file

@ -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

View file

@ -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)

View file

@ -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
}

View file

@ -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
}

View file

@ -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

View file

@ -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) {

View file

@ -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,
}

View file

@ -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,

View file

@ -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
}

View file

@ -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,
},
}

View file

@ -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',
},
},