Skip to content

Commit

Permalink
Prevent unauthorized github users from logging in & enable GHE
Browse files Browse the repository at this point in the history
  • Loading branch information
tortilaman committed Jul 21, 2017
1 parent 2066024 commit 048fb04
Show file tree
Hide file tree
Showing 20 changed files with 207 additions and 49 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ npm-debug.log
.tern-project
yarn-error.log
.vscode/
manifest.yml
.imdone/
config.local.yml
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ please read the [code of conduct](CODE_OF_CONDUCT.md).

## Setup

> Install yarn on your system: https://yarnpkg.com/en/docs/install
> Install yarn on your system: [https://yarnpkg.com/en/docs/install](https://yarnpkg.com/en/docs/install)
```sh
$ git clone https://github.com/netlify/netlify-cms
Expand Down Expand Up @@ -33,7 +33,7 @@ $ npm run test:watch
$ npm run lint
```

## Runing the server
## Running the server

```sh
$ npm run start
Expand Down
33 changes: 33 additions & 0 deletions docs/custom-authentication.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Custom Authentication

Netlify CMS is meant to be platform agnostic, so we're always looking to expand the ecosystem and find new ways to use it. Below is a list of currently submitted OAuth providers - feel free to [submit a pull request](https://github.com/netlify/netlify-cms/blob/master/CONTRIBUTING.md) if you'd like to add yours!

## External OAuth Clients:
| Author | Supported Git hosts | Languages | Link |
|------------|---------------------------|-----------|---------------------------------------------------------------------|
| @vencax | GitHub, GitHub Enterprise | Node.js | [Repo](https://github.com/vencax/netlify-cms-github-oauth-provider) |

Check each project's readme for instructions on how to configure it.

## Configuration
CMS configuration properties that affect authentication, including some optional properties that aren't mentioned elsewhere in the docs, are explained below:

```yaml
backend:

# REQUIRED CONFIG
name: github
repo: user/repository

# OPTIONAL CONFIG
# Note: no trailing slashes on URLs
api_root: https://github.some.domain.com/api/v3
site_domain: static.site.url.com
base_url: https://auth.server.url.com
```
* **name** name of the auth provider, varies by implementation. `github` when using GitHub auth, even with a third party auth client.
* **repo** repo where content is to be stored.
* **api_root (optional)** the API endpoint. Defaults to `https://api.github.com` when used with the `github` provider. Only necessary in certain cases, eg. when using with GitHub Enterprise.
* **site_domain (optional)** sets `site_id` query param sent to API endpoint, defaults to `location.hostname`, minus any port, or `cms.netlify.com` on localhost so that auth "just works" during local development. Sites with custom authentication will often need to set this for local development to work properly.
* **base_url (optional)** OAuth client URL, defaults to `https:/api.netlify.com` as a convenience. This is **required** when using an external OAuth server.
19 changes: 17 additions & 2 deletions docs/extending.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,16 @@ Although possible, it may be cumbersome or even impractical to add a React build
Register a custom widget.

```js
CMS.registerWidget(field, control, \[preview\])
CMS.registerWidget(name, control, \[preview\])
```

**Params:**

Param | Type | Description
--- | --- | ---
`name` | string | Widget name, allows this widget to be used via the field `widget` property in config
`control` | React.Component \| string | <ul><li>React component that renders the control, receives the following props: <ul><li>**value:** Current field value</li><li>**onChange**: Callback function to update the field value</li></ul></li><li>Name of a registered widget whose control should be used (includes built in widgets).</li></ul>
[`preview`] | React.Component, optional | Renders the widget preview, receives the following props: <ul><li>**value:** Current preview value</li><li>**field:** Immutable map of current field configuration</li><li>**metadata:** Immutable map of any available metadata for the current field</li><li>**getAsset:** Function for retrieving an asset url for image/file fields</li><li>**entry:** Immutable Map of all entry data</li><li>**fieldsMetaData:** Immutable map of metadata from all fields.</li></ul>
* **field:** The field type which this widget will be used for.
* **control:** A React component that renders the editing interface for this field. Two props will be passed:
* **value:** The current value for this field.
Expand All @@ -43,7 +48,17 @@ var CategoriesControl = createClass({
}
});
CMS.registerWidget('categories', CategoriesControl);
var CategoriesPreview = createClass({
render: function() {
return h('ul', {},
this.props.value.map(function(val, index) {
return h('li', {key: index}, val);
})
);
}
});
CMS.registerWidget('categories', CategoriesControl, CategoriesPreview);
</script>
```

Expand Down
14 changes: 1 addition & 13 deletions docs/intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,19 +87,7 @@ You point to where the files are stored, and specify the fields that define them

### Widgets

Widgets define the data type and interface for entry fields. Netlify CMS comes with several built-in widgets, including:

Widget | UI | Data Type
--- | --- | ---
`string` | text input | string
`text` | text area input | plain text, multiline input
`number` | text input with `+` and `-` buttons | number
`markdown` | rich text editor with raw option | markdown-formatted string
`datetime` | date picker widget | ISO date string
`image` | file picker widget with drag-and-drop | file path saved as string, image uploaded to media folder
`hidden` | No UI | Hidden element, typically only useful with a `default` attribute

We’re always adding new widgets, and you can also create your own.
Widgets define the data type and interface for entry fields. Netlify CMS comes with several built-in [widgets](/docs/widgets).

## Customization

Expand Down
71 changes: 56 additions & 15 deletions docs/widgets.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,60 @@

Widgets define the data type and interface for entry fields. Netlify CMS comes with several built-in widgets, including:

Widget | UI | Data Type
| Name | UI | Data Type |
| -------- | ---------------------------------- | ---------------------------------------------------|
| `string` | text input | string |
| `boolean` | toggle switch | boolean |
| `text` | textarea input | string (multiline) |
| `number` | number input | number |
| `markdown` | rich text editor | string (markdown) |
| `datetime` | date picker | string (ISO date) |
| `select` | select input (dropdown) | string |
| `image` | file picker w/ drag and drop | image file |
| `file` | file picker w/ drag and drop | file |
| `hidden` | none | string |
| `object` | group of other widgets | Immutable Map containing field values |
| `list` | repeatable group of other widgets | Immutable List of objects containing field values |
| `relation` | text input w/ suggestions dropdown | value of `valueField` in related entry (see below) |

We’re always adding new widgets, and you can also [create your own](/docs/extending)!

### Relation Widget

The relation widget allows you to reference an existing entry from within the entry you're editing. It provides a search input with a list of entries from the collection you're referencing, and the list automatically updates with matched entries based on what you've typed.

The following field configuration properties are specific to fields using the relation widget:

Property | Accepted Values | Description
--- | --- | ---
`string` | text input | string
`boolean` | select input | switch for true and false
`text` | text area input | plain text, multiline input
`number` | text input with `+` and `-` buttons | number
`markdown` | rich text editor with raw option | markdown-formatted string
`datetime` | date picker widget | ISO date string
`image` | file picker widget with drag-and-drop | file path saved as string, image uploaded to media folder
`hidden` | No UI | Hidden element, typically only useful with a `default` attribute
`list` | text input | strings separated by commas
`file` | file input | input file upload
`select` | select input | dropdown filled with `options` from the config
`object` | custom | `fields` as a list defined in the config

We’re always adding new widgets, and you can also [create your own](/docs/extending).
`collection` | string | name of the collection being referenced
`searchFields` | list | one or more names of fields in the referenced colleciton to search for the typed value
`valueField` | string | name a field from the referenced collection whose value will be stored for the relation
`name` | text input | string

Let's say we have a "posts" collection and an "authors" collection, and we want to select an author for each post - our config might look something like this:

```yaml
collections:
- name: authors
label: Authors
folder: "authors"
create: true
fields:
- {name: name, label: Name}
- {name: twitterHandle, label: "Twitter Handle"}
- {name: bio, label: Bio, widget: text}
- name: posts
label: Posts
folder: "posts"
create: true
fields:
- {name: title, label: Title}
- {name: body, label: Body, widget: markdown}
- name: author
label: Author
widget: relation
collection: authors
searchFields: [name, twitterHandle]
valueField: name
```
18 changes: 18 additions & 0 deletions example/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ collections: # A list of collections the CMS should be able to edit
folder: "_sink"
create: true
fields:
- label: "Related Post"
name: "post"
widget: "relationKitchenSinkPost"
collection: "posts"
searchFields: ["title", "body"]
valueField: "title"
- {label: "Title", name: "title", widget: "string"}
- {label: "Boolean", name: "boolean", widget: "boolean", default: true}
- {label: "Text", name: "text", widget: "text"}
Expand All @@ -77,6 +83,12 @@ collections: # A list of collections the CMS should be able to edit
name: "object"
widget: "object"
fields:
- label: "Related Post"
name: "post"
widget: "relationKitchenSinkPost"
collection: "posts"
searchFields: ["title", "body"]
valueField: "title"
- {label: "String", name: "string", widget: "string"}
- {label: "Boolean", name: "boolean", widget: "boolean", default: false}
- {label: "Text", name: "text", widget: "text"}
Expand Down Expand Up @@ -119,6 +131,12 @@ collections: # A list of collections the CMS should be able to edit
name: "list"
widget: "list"
fields:
- label: "Related Post"
name: "post"
widget: "relationKitchenSinkPost"
collection: "posts"
searchFields: ["title", "body"]
valueField: "title"
- {label: "String", name: "string", widget: "string"}
- {label: "Boolean", name: "boolean", widget: "boolean"}
- {label: "Text", name: "text", widget: "text"}
Expand Down
24 changes: 23 additions & 1 deletion example/index.html

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions src/actions/auth.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { currentBackend } from '../backends/backend';
import { actions as notifActions } from 'redux-notifications';

const { notifSend } = notifActions;

export const AUTH_REQUEST = 'AUTH_REQUEST';
export const AUTH_SUCCESS = 'AUTH_SUCCESS';
Expand Down Expand Up @@ -60,6 +63,11 @@ export function loginUser(credentials) {
dispatch(authenticate(user));
})
.catch((error) => {
dispatch(notifSend({
message: `${ error.message }`,
kind: 'warning',
dismissAfter: 8000,
}));
dispatch(authError(error));
});
};
Expand Down
11 changes: 8 additions & 3 deletions src/backends/github/API.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ export default class API {
return this.request("/user");
}

collaborator(user) {
return this.request(`${ this.repoURL }/collaborators/${ user }`);
}

requestHeaders(headers = {}) {
const baseHeader = {
"Content-Type": "application/json",
Expand Down Expand Up @@ -64,7 +68,9 @@ export default class API {
return fetch(url, { ...options, headers }).then((response) => {
responseStatus = response.status;
const contentType = response.headers.get("Content-Type");
if (contentType && contentType.match(/json/)) {
if (url.indexOf('/collaborators/') >= 0) {
return responseStatus;
} else if (contentType && contentType.match(/json/)) {
return this.parseJsonResponse(response);
}
return response.text();
Expand Down Expand Up @@ -236,7 +242,6 @@ export default class API {
persistFiles(entry, mediaFiles, options) {
const uploadPromises = [];
const files = mediaFiles.concat(entry);


files.forEach((file) => {
if (file.uploaded) { return; }
Expand Down Expand Up @@ -343,7 +348,7 @@ export default class API {

deleteUnpublishedEntry(collection, slug) {
const contentKey = slug;
let prNumber;
let prNumber;
return this.retrieveMetadata(contentKey)
.then(metadata => this.closePR(metadata.pr, metadata.objects))
.then(() => this.deleteBranch(`cms/${ contentKey }`));
Expand Down
1 change: 1 addition & 0 deletions src/backends/github/AuthenticationPage.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.root {
display: flex;
flex-flow: column nowrap;
align-items: center;
justify-content: center;
height: 100vh;
Expand Down
7 changes: 5 additions & 2 deletions src/backends/github/AuthenticationPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import React from 'react';
import Button from 'react-toolbox/lib/button';
import Authenticator from '../../lib/netlify-auth';
import { Icon } from '../../components/UI';
import { Notifs } from 'redux-notifications';
import { Toast } from '../../components/UI/index';
import styles from './AuthenticationPage.css';

export default class AuthenticationPage extends React.Component {
static propTypes = {
onLogin: React.PropTypes.func.isRequired,
onLogin: React.PropTypes.func.isRequired
};

state = {};
Expand All @@ -16,7 +18,7 @@ export default class AuthenticationPage extends React.Component {
const cfg = {
base_url: this.props.base_url,
site_id: (document.location.host.split(':')[0] === 'localhost') ? 'cms.netlify.com' : this.props.siteId
}
};
const auth = new Authenticator(cfg);

auth.authenticate({ provider: 'github', scope: 'repo' }, (err, data) => {
Expand All @@ -33,6 +35,7 @@ export default class AuthenticationPage extends React.Component {

return (
<section className={styles.root}>
<Notifs CustomComponent={Toast} />
{loginError && <p>{loginError}</p>}
<Button
className={styles.button}
Expand Down
20 changes: 15 additions & 5 deletions src/backends/github/implementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export default class GitHub {

this.repo = config.getIn(["backend", "repo"], "");
this.branch = config.getIn(["backend", "branch"], "master");
this.api_root = config.getIn(["backend", "api_root"], "https://api.github.com");
this.token = '';
}

Expand All @@ -29,11 +30,20 @@ export default class GitHub {

authenticate(state) {
this.token = state.token;
this.api = new API({ token: this.token, branch: this.branch, repo: this.repo });
return this.api.user().then((user) => {
user.token = state.token;
return user;
});
this.api = new API({ token: this.token, branch: this.branch, repo: this.repo, api_root: this.api_root });
return this.api.user().then(user =>
this.api.collaborator(user.login).then((status) => {
if (status === 404 || status === 403) {
// Unauthorized user
throw new Error("Your GitHub user account does not have access to this repo.");
} else if (status === 204) {
// Authorized user
user.token = state.token;
return user;
}
throw new Error('GitHub is not responding, please try again.');
})
);
}

getToken() {
Expand Down
3 changes: 3 additions & 0 deletions src/components/EntryListing/EntryListing.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ export default class EntryListing extends React.Component {
const title = label || entry.getIn(['data', inferedFields.titleField]);
let image = entry.getIn(['data', inferedFields.imageField]);
image = resolvePath(image, publicFolder);
if(image) {
image = encodeURI(image);
}

return (
<Card
Expand Down
4 changes: 3 additions & 1 deletion src/components/PreviewPane/PreviewPane.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import styles from './PreviewPane.css';
export default class PreviewPane extends React.Component {

getWidget = (field, value, props) => {
const { fieldsMetaData, getAsset } = props;
const { fieldsMetaData, getAsset, entry } = props;
const widget = resolveWidget(field.get('widget'));

return !widget.preview ? null : React.createElement(widget.preview, {
Expand All @@ -22,6 +22,8 @@ export default class PreviewPane extends React.Component {
value: value && Map.isMap(value) ? value.get(field.get('name')) : value,
metadata: fieldsMetaData && fieldsMetaData.get(field.get('name')),
getAsset,
entry,
fieldsMetaData,
});
};

Expand Down
1 change: 1 addition & 0 deletions src/components/Widgets/ListControl.css
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

.addButtonText {
margin-left: 4px;
text-transform: lowercase;
}

.removeButton {
Expand Down
Loading

0 comments on commit 048fb04

Please sign in to comment.