Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP DO NOT REVIEW: Browser authorization flow for clients #528

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion babel.config.js
Expand Up @@ -8,7 +8,7 @@ const config = {
'@babel/preset-env',
{
useBuiltIns: 'entry',
modules: false,
modules: 'commonjs',
},
],
],
Expand Down
26 changes: 19 additions & 7 deletions cmd/frontend/graphqlbackend/schema.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 19 additions & 7 deletions cmd/frontend/graphqlbackend/schema.graphql
Expand Up @@ -153,14 +153,26 @@ type Mutation {
# "subject" user after token creation). The result is the access token value, which the caller is responsible
# for storing (it is not accessible by Sourcegraph after creation).
#
# The supported scopes are:
#
# - "user:all": Full control of all resources accessible to the user account.
# - "site-admin:sudo": Ability to perform any action as any other user. (Only site admins may create tokens
# with this scope.)
#
# Only the user or site admins may perform this mutation.
createAccessToken(user: ID!, scopes: [String!]!, note: String!): CreateAccessTokenResult!
createAccessToken(
# The user whose privileges are granted to the holder of the access token.
user: ID!
# The scopes, which specify the operations permitted to the holder of the access token.
#
# The supported scopes are:
#
# - "user:all": Full control of all resources accessible to the user account.
# - "site-admin:sudo": Ability to perform any action as any other user. (Only site admins may create tokens
# with this scope.)
scopes: [String!]!
# A note describing the purpose of this access token.
note: String!
# An optional nonce that ties this access token to the client application that initiated the authorization
# flow to create it. If given, then the holder of the requestSession value may read the access token from
# the Sourcegraph instance (without any other authorization). This allows client applications to perform a
# browser-based authorization flow to obtain an access token for the current user.
requestSession: String
): CreateAccessTokenResult!
# Deletes and immediately revokes the specified access token, specified by either its ID or by the token
# itself.
#
Expand Down
1 change: 1 addition & 0 deletions dev/config.json
Expand Up @@ -2,6 +2,7 @@
{
"appURL": "http://localhost:3080",
"experimentalFeatures": {
"clientAuthorizationFlow": true,
"discussions": "enabled"
},
"disablePublicRepoRedirects": true,
Expand Down
21 changes: 21 additions & 0 deletions doc/integration/src_cli.md
@@ -0,0 +1,21 @@
# `src` CLI

The [`src` CLI tool](https://github.com/sourcegraph/src-cli) provides access to Sourcegraph via a command-line interface.

# Browser authorization flow for clients

It's now easier to use the Sourcegraph [`src` CLI](https://github.com/sourcegraph/src-cli) and other client applications that require authorization with your Sourcegraph instance. Sourcegraph now supports a browser-based authorization flow, so you can just approve the request in your browser (and don't need to manually generate an access token).

Here's how it looks for the `src` CLI:

1. Run `src init` to open your web browser to the Sourcegraph browser-based authorization flow for Sourcegraph.com.

Run `src init --url https://sourcegraph.example.com` for a self-hosted Sourcegraph instance.

If you're in a terminal with no web browser, use the `--console-only` flag to print the URL to open manually.
1. Follow the prompts to sign in (if needed).
1. Review and approve the authorization request.

When finished, the `src` CLI will be authorized (with an access token that you can revoke at any time in your user account area). Try running `src config get` to get your user settings, for example.

If you've used the [`gcloud init` command](https://cloud.google.com/sdk/docs/initializing) with Google Cloud, this process will be familiar.
34 changes: 34 additions & 0 deletions package.json
Expand Up @@ -52,6 +52,31 @@
".ts"
]
},
"jest": {
"moduleFileExtensions": [
"ts",
"tsx",
"js"
],
"testEnvironment": "node",
"preset": "ts-jest/presets/js-with-babel",
"globals": {
"ts-jest": {
"tsConfig": "tsconfig.test.json"
}
},
"testMatch": [
"**/*.test.ts?(x)"
],
"testPathIgnorePatterns": [
"/node_modules/",
"/src/repo/index.test.tsx",
"/src/util/dom.test.tsx"
],
"transformIgnorePatterns": [
"xfidufufsd"
]
},
"browserslist": [
"last 1 version",
">1%",
Expand Down Expand Up @@ -124,6 +149,7 @@
"whatwg-url": "^7.0.0"
},
"devDependencies": {
"@babel/plugin-transform-modules-commonjs": "^7.1.0",
"@babel/preset-env": "^7.0.0",
"@babel/preset-react": "^7.0.0",
"@gql2ts/from-schema": "^1.10.1",
Expand All @@ -141,6 +167,8 @@
"@types/d3-scale": "2.0.2",
"@types/d3-selection": "1.3.2",
"@types/d3-shape": "1.2.4",
"@types/enzyme": "^3.1.14",
"@types/enzyme-adapter-react-16": "^1.0.3",
"@types/execa": "0.9.0",
"@types/fancy-log": "1.3.0",
"@types/get-stream": "3.0.1",
Expand Down Expand Up @@ -169,15 +197,19 @@
"@types/webpack": "4.4.17",
"@types/webpack-dev-server": "^3.1.1",
"autoprefixer": "^9.0.0",
"babel-jest": "^23.6.0",
"babel-loader": "^8.0.0",
"babel-plugin-lodash": "^3.3.4",
"bundlesize": "^0.17.0",
"cpy": "^7.0.1",
"cross-env": "^5.2.0",
"css-loader": "^1.0.0",
"enzyme": "^3.7.0",
"enzyme-adapter-react-16": "^1.6.0",
"execa": "^1.0.0",
"globby": "^8.0.1",
"gulp": "^4.0.0",
"jest": "^23.6.0",
"json-schema-to-typescript": "^5.2.2",
"koa-connect": "^2.0.1",
"latest-version": "^4.0.0",
Expand All @@ -194,6 +226,8 @@
"style-loader": "^0.23.1",
"stylelint": "^9.4.0",
"stylelint-formatter-compact": "^1.1.0",
"thread-loader": "sourcegraph/thread-loader#6321efbc3524f18cd823dd300c1a8a7fe82ca3a5",
"ts-jest": "^23.10.4",
"ts-loader": "^5.0.0",
"ts-node": "^7.0.1",
"ts-unused-exports": "^2.0.5",
Expand Down
6 changes: 6 additions & 0 deletions pkg/conf/computed.go
Expand Up @@ -42,6 +42,12 @@ func JumpToDefOSSIndexEnabled() bool {
return p == "enabled"
}

// ClientAuthorizationFlowEnabled reports whether the clientAuthorizationFlow experiment is enabled.
func ClientAuthorizationFlowEnabled() bool {
p := Get().ExperimentalFeatures.ClientAuthorizationFlow
return p != nil && *p // default is disabled
}

type AccessTokAllow string

const (
Expand Down
7 changes: 4 additions & 3 deletions schema/schema.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions schema/site.schema.json
Expand Up @@ -80,6 +80,13 @@
"enum": ["enabled", "disabled"],
"default": "enabled"
},
"clientAuthorizationFlow": {
"description":
"Enables the client authorization flow, which lets client applications initiate a browser-based authorization flow and obtain an access token for the current user (if the user approves the authorization request).",
"type": "boolean",
"!go": { "pointer": true },
"default": false
},
"discussions": {
"description": "Enables the code discussions experiment.",
"type": "string",
Expand Down
7 changes: 7 additions & 0 deletions schema/site_stringdata.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions src/auth.ts
Expand Up @@ -52,7 +52,9 @@ export function refreshAuthenticatedUser(): Observable<never> {
)
}

const initialSiteConfigAuthPublic = window.context.site['auth.public']
const initialSiteConfigAuthPublic = Boolean(
typeof window !== 'undefined' && window.context && window.context.site && window.context.site['auth.public']
)

/**
* Whether auth is required to perform any action.
Expand All @@ -66,7 +68,7 @@ const initialSiteConfigAuthPublic = window.context.site['auth.public']
export const authRequired = authenticatedUser.pipe(map(user => user === null && !initialSiteConfigAuthPublic))

// Populate authenticatedUser.
if (window.context.isAuthenticatedUser) {
if (typeof window !== 'undefined' && window.context && window.context.isAuthenticatedUser) {
refreshAuthenticatedUser()
.toPromise()
.then(() => void 0, err => console.error(err))
Expand Down
39 changes: 39 additions & 0 deletions src/auth/ClientAuthorizationFlow.test.tsx
@@ -0,0 +1,39 @@
import assert from 'assert'
import Enzyme, { shallow } from 'enzyme'
import Adapter from 'enzyme-adapter-react-16'
import H from 'history'
import React from 'react'
import { Redirect } from 'react-router'
// import sinon from 'sinon'
import { ClientAuthorizationFlow } from './ClientAuthorizationFlow'

Enzyme.configure({ adapter: new Adapter() })

describe('<ClientAuthorizationFlow />', () => {
it('renders three <Foo /> components', () => {
const wrapper = shallow(<ClientAuthorizationFlow location={H.createLocation('/')} authenticatedUser={null} />)
assert.strictEqual(wrapper.find(Redirect).length, 1)
})

/* it('renders an `.icon-star`', () => {
const wrapper = shallow(<ClientAuthorizationFlow />);
expect(wrapper.find('.icon-star')).to.have.lengthOf(1);
});

it('renders children when passed in', () => {
const wrapper = shallow((
<ClientAuthorizationFlow>
<div className="unique" />
</ClientAuthorizationFlow>
));
expect(wrapper.contains(<div className="unique" />)).to.equal(true);
});

it('simulates click events', () => {
const onButtonClick = sinon.spy();
const wrapper = shallow(<Foo onButtonClick={onButtonClick} />);
wrapper.find('button').simulate('click');
expect(onButtonClick).to.have.property('callCount', 1);
});
*/
})