Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 35 additions & 6 deletions docs/Contributing/API-for-contributors.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Unlike the [Fleet REST API documentation](../Using-Fleet/REST-API.md), only the
- [Get device's policies](#get-devices-policies)
- [Get device's API features](#get-devices-api-features)
- [Get device's transparency URL](#get-devices-transparency-url)
- [Check if an installer exists](#check-if-an-installer-exists)
- [Download an installer](#download-an-installer)

### Get queries spec
Expand Down Expand Up @@ -1717,15 +1718,16 @@ Redirects to the transparency URL.

Downloads a pre-built fleet-osquery installer with the given parameters.

`GET /api/_version_/fleet/download_installer/{kind}`
`POST /api/_version_/fleet/download_installer/{kind}`

#### Parameters

| Name | Type | In | Description |
| ------------- | ------- | ----- | ------------------------------------------------------------------ |
| kind | string | path | The installer kind: pkg, msi, deb or rpm. |
| enroll_secret | string | query | The global enroll secret. |
| desktop | boolean | query | Set to `true` to ask for an installer that includes Fleet Desktop. |
| Name | Type | In | Description |
| ------------- | ------- | ---------------------- | ------------------------------------------------------------------ |
| kind | string | path | The installer kind: pkg, msi, deb or rpm. |
| enroll_secret | string | x-www-form-urlencoded | The global enroll secret. |
| token | string | x-www-form-urlencoded | The authentication token. |
Comment thread
lucasmrod marked this conversation as resolved.
| desktop | boolean | x-www-form-urlencoded | Set to `true` to ask for an installer that includes Fleet Desktop. |

##### Default response

Expand All @@ -1746,4 +1748,31 @@ If an installer with the provided parameters is found, the installer is returned
This error occurs if an installer with the provided parameters doesn't exist.


### Check if an installer exists

Checks if a pre-built fleet-osquery installer with the given parameters exists.

`HEAD /api/_version_/fleet/download_installer/{kind}`

#### Parameters

| Name | Type | In | Description |
| ------------- | ------- | ----- | ------------------------------------------------------------------ |
| kind | string | path | The installer kind: pkg, msi, deb or rpm. |
| enroll_secret | string | query | The global enroll secret. |
| desktop | boolean | query | Set to `true` to ask for an installer that includes Fleet Desktop. |

##### Default response

`Status: 200`

If an installer with the provided parameters is found.

##### Installer doesn't exist

`Status: 400`

If an installer with the provided parameters doesn't exist.


<meta name="pageOrderInSection" value="800">
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import React, { useState } from "react";
import FileSaver from "file-saver";
import React, { FunctionComponent, useState } from "react";

import {
IInstallerPlatform,
IInstallerType,
INSTALLER_PLATFORM_BY_TYPE,
INSTALLER_TYPE_BY_PLATFORM,
} from "interfaces/installer";
import ENDPOINTS from "utilities/endpoints";
import local from "utilities/local";
import URL_PREFIX from "router/url_prefix";
import installerAPI from "services/entities/installers";

import Button from "components/buttons/Button";
Expand All @@ -28,6 +30,17 @@ interface IDownloadInstallersProps {
onCancel: () => void;
}

interface IDownloadFormProps {
url: string;
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
token: string | null;
enrollSecret: string;
includeDesktop: boolean;
selectedInstaller: string | undefined;
isDownloading: boolean;
isDownloadSuccess: boolean;
}

const baseClass = "download-installers";

const displayOrder = [
Expand Down Expand Up @@ -60,37 +73,81 @@ const displayIcon = (platform: IInstallerPlatform, isSelected: boolean) => {
}
};

const DownloadForm: FunctionComponent<IDownloadFormProps> = ({
url,
onSubmit,
token,
enrollSecret,
includeDesktop,
selectedInstaller,
isDownloading,
isDownloadSuccess,
}) => {
return (
<form
key="form"
method="POST"
action={url}
target="auxFrame"
onSubmit={onSubmit}
>
<input type="hidden" name="token" value={token || ""} />
<input type="hidden" name="enroll_secret" value={enrollSecret} />
<input type="hidden" name="desktop" value={String(includeDesktop)} />
<iframe title="auxFrame" name="auxFrame" />
{!isDownloadSuccess && (
<Button
className={`${baseClass}__button--download`}
disabled={!selectedInstaller}
type="submit"
>
{isDownloading ? <Spinner /> : "Download installer"}
</Button>
)}
</form>
);
};

const DownloadInstallers = ({
enrollSecret,
onCancel,
}: IDownloadInstallersProps): JSX.Element => {
const [includeDesktop, setIncludeDesktop] = useState(true);
const [isDownloadError, setIsDownloadError] = useState(false);
const [isDownloading, setIsDownloading] = useState(false);
const [isDownloadError, setIsDownloadError] = useState(false);
const [isDownloadSuccess, setIsDownloadSuccess] = useState(false);
const [selectedInstaller, setSelectedInstaller] = useState<
IInstallerType | undefined
>();
const path = `${ENDPOINTS.DOWNLOAD_INSTALLER}/${selectedInstaller}`;
const { origin } = global.window.location;
const url = `${origin}${URL_PREFIX}/api${path}`;
const token = local.getItem("auth_token");

const downloadInstaller = async (installerType?: IInstallerType) => {
if (!installerType) {
const downloadInstaller = async (event: React.FormEvent<HTMLFormElement>) => {
if (!selectedInstaller) {
// do nothing
return;
}

// Prevent the submit behavior, as we want to control when the POST is
// actually performed.
event.preventDefault();
event.persist();

setIsDownloading(true);
try {
const blob: BlobPart = await installerAPI.downloadInstaller({
// First check if the installer exists, no need to save the result of
// this operation as any status other than 200 will throw an error
await installerAPI.checkInstallerExistence({
enrollSecret,
installerType,
includeDesktop,
installerType: selectedInstaller,
});
const filename = `fleet-osquery.${installerType}`;
const file = new global.window.File([blob], filename, {
type: "application/octet-stream",
});
FileSaver.saveAs(file);

(event.target as HTMLFormElement).submit();
setIsDownloadSuccess(true);
} catch {
} catch (error) {
setIsDownloadError(true);
} finally {
setIsDownloading(false);
Expand All @@ -109,6 +166,20 @@ const DownloadInstallers = ({
setSelectedInstaller(type);
};

const form = (
<DownloadForm
key="downloadForm"
url={url}
onSubmit={downloadInstaller}
token={token}
enrollSecret={enrollSecret}
includeDesktop={includeDesktop}
selectedInstaller={selectedInstaller}
isDownloading={isDownloading}
isDownloadSuccess={isDownloadSuccess}
/>
);

if (isDownloadError) {
return (
<div className={`${baseClass}__error`}>
Expand All @@ -128,6 +199,7 @@ const DownloadInstallers = ({
<h2>You&rsquo;re almost there</h2>
<p>{`Run the installer on a ${installerPlatform}laptop, workstation, or sever to add it to Fleet.`}</p>
<Button onClick={onCancel}>Got it</Button>
{form}
</div>
);
}
Expand Down Expand Up @@ -171,13 +243,7 @@ const DownloadInstallers = ({
</TooltipWrapper>
</>
</Checkbox>
<Button
className={`${baseClass}__button--download`}
disabled={!selectedInstaller}
onClick={() => downloadInstaller(selectedInstaller)}
>
{isDownloading ? <Spinner /> : "Download installer"}
</Button>
{form}
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
padding: 0;
padding-bottom: 20px;

iframe {
display: none;
}

p {
padding-top: $pad-small;
padding-bottom: $pad-medium;
Expand Down Expand Up @@ -97,6 +101,10 @@
margin: 0;
padding-bottom: 24px;
}

iframe {
display: none;
}
}

&__error {
Expand Down
9 changes: 4 additions & 5 deletions frontend/services/entities/installers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,24 @@ import { IInstallerType } from "interfaces/installer";
import sendRequest from "services";
import ENDPOINTS from "utilities/endpoints";

export interface IDownloadInstallerRequestParams {
export interface ICheckInstallerExistenceRequestParams {
enrollSecret: string;
includeDesktop: boolean;
installerType: IInstallerType;
}

export default {
downloadInstaller: ({
checkInstallerExistence: ({
enrollSecret,
includeDesktop,
installerType,
}: IDownloadInstallerRequestParams): Promise<BlobPart> => {
}: ICheckInstallerExistenceRequestParams): Promise<BlobPart> => {
const path = `${
ENDPOINTS.DOWNLOAD_INSTALLER
}/${installerType}?desktop=${includeDesktop}&enroll_secret=${encodeURIComponent(
enrollSecret
)}`;
console.log("path: ", path);

return sendRequest("GET", path, undefined, "blob");
return sendRequest("HEAD", path, undefined);
},
};
2 changes: 1 addition & 1 deletion frontend/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import local from "utilities/local";
import URL_PREFIX from "router/url_prefix";

const sendRequest = async (
method: "GET" | "POST" | "PATCH" | "DELETE",
method: "GET" | "POST" | "PATCH" | "DELETE" | "HEAD",
path: string,
data?: unknown,
responseType: AxiosResponseType = "json"
Expand Down
8 changes: 6 additions & 2 deletions server/contexts/token/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,16 @@ const tokenKey key = 0
type Token string

// FromHTTPRequest extracts an Authorization
// from an HTTP header if present.
// from an HTTP request if present.
func FromHTTPRequest(r *http.Request) Token {
headers := r.Header.Get("Authorization")
headerParts := strings.Split(headers, " ")
if len(headerParts) != 2 || strings.ToUpper(headerParts[0]) != "BEARER" {
return ""
if err := r.ParseForm(); err != nil {
return ""
}

return Token(r.FormValue("token"))
}
return Token(headerParts[1])
}
Expand Down
19 changes: 19 additions & 0 deletions server/datastore/s3/installer.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,25 @@ func (i *InstallerStore) Put(ctx context.Context, installer fleet.Installer) (st
return key, err
}

// Exists checks if an installer exists in the S3 bucket
func (i *InstallerStore) Exists(ctx context.Context, installer fleet.Installer) (bool, error) {
key := i.keyForInstaller(installer)
_, err := i.s3client.HeadObject(&s3.HeadObjectInput{Bucket: &i.bucket, Key: &key})

if err != nil {
if aerr, ok := err.(awserr.Error); ok {
switch aerr.Code() {
case s3.ErrCodeNoSuchKey, s3.ErrCodeNoSuchBucket, "NotFound":
return false, nil
}
}

return false, ctxerr.Wrap(ctx, err, "checking existence on file store")
}

return true, nil
}

// keyForInstaller builds an S3 key to search for the installer
func (i *InstallerStore) keyForInstaller(installer fleet.Installer) string {
file := fmt.Sprintf("%s.%s", executable, installer.Kind)
Expand Down
1 change: 1 addition & 0 deletions server/fleet/datastore.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type CarveStore interface {
type InstallerStore interface {
Get(ctx context.Context, installer Installer) (io.ReadCloser, int64, error)
Put(ctx context.Context, installer Installer) (string, error)
Exists(ctx context.Context, installer Installer) (bool, error)
}

// Datastore combines all the interfaces in the Fleet DAL
Expand Down
1 change: 1 addition & 0 deletions server/fleet/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -485,4 +485,5 @@ type Service interface {
// Installers

GetInstaller(ctx context.Context, installer Installer) (io.ReadCloser, int64, error)
CheckInstallerExistence(ctx context.Context, installer Installer) error
}
10 changes: 10 additions & 0 deletions server/mock/datastore_installers.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,17 @@ type GetFunc func(ctx context.Context, installer fleet.Installer) (io.ReadCloser

type PutFunc func(ctx context.Context, installer fleet.Installer) (string, error)

type ExistsFunc func(ctx context.Context, installer fleet.Installer) (bool, error)

type InstallerStore struct {
GetFunc GetFunc
GetFuncInvoked bool

PutFunc PutFunc
PutFuncInvoked bool

ExistsFunc ExistsFunc
ExistsFuncInvoked bool
}

func (s *InstallerStore) Get(ctx context.Context, installer fleet.Installer) (io.ReadCloser, int64, error) {
Expand All @@ -32,3 +37,8 @@ func (s *InstallerStore) Put(ctx context.Context, installer fleet.Installer) (st
s.PutFuncInvoked = true
return s.PutFunc(ctx, installer)
}

func (s *InstallerStore) Exists(ctx context.Context, installer fleet.Installer) (bool, error) {
s.ExistsFuncInvoked = true
return s.ExistsFunc(ctx, installer)
}
4 changes: 4 additions & 0 deletions server/service/endpoint_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,10 @@ func (e *authEndpointer) DELETE(path string, f handlerFunc, v interface{}) {
e.handleEndpoint(path, f, v, "DELETE")
}

func (e *authEndpointer) HEAD(path string, f handlerFunc, v interface{}) {
e.handleEndpoint(path, f, v, "HEAD")
}

// PathHandler registers a handler for the verb and path. The pathHandler is
// a function that receives the actual path to which it will be mounted, and
// returns the actual http.Handler that will handle this endpoint. This is for
Expand Down
Loading