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

Factor out api hostname #46

Merged
merged 9 commits into from
Mar 11, 2024
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
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,28 @@ current branch. Missing RFDs will 404. If you are working on two RFDs and they'r
different branches, you cannot preview both at the same time unless you make a temporary
combined branch that contains both.

### Configuration

When running in a non-local mode, the following settings must be specified:

- `SESSION_SECRET` - Key that will be used to signed cookies

- `RFD_API` - Backend RFD API to communicate with (i.e. https://api.server.com)
- `RFD_API_CLIENT_ID` - OAuth client id create via the RFD API
- `RFD_API_CLIENT_SECRET` - OAuth client secret create via the RFD API
- `RFD_API_GOOGLE_CALLBACK_URL` - Should be of the form of
`https://{rfd_site_hostname}/auth/google/callback`
- `RFD_API_GITHUB_CALLBACK_URL` - Should be of the form of
`https://{rfd_site_hostname}/auth/github/callback`

- `STORAGE_URL` - Url of bucket for static assets
- `STORAGE_KEY_NAME` - Name of the key defined in `STORAGE_KEY`
- `STORAGE_KEY` - Key for generating signed static asset urls

- `GITHUB_APP_ID` - App id for fetching GitHub PR discussions
- `GITHUB_INSTALLATION_ID` - Installation id of GitHub App
- `GITHUB_PRIVATE_KEY` - Private key of the GitHub app for discussion fetching

## License

Unless otherwise noted, all components are licensed under the
Expand Down
4 changes: 3 additions & 1 deletion app/services/authn.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
import { returnToCookie } from './cookies.server'
import { getUserRedirect } from './redirect.server'
import { isLocalMode } from './rfd.server'
import { apiRequest } from './rfdApi.server'
import { apiRequest, getRfdApiUrl } from './rfdApi.server'

export type AuthenticationService = 'github' | 'google' | 'local'

Expand Down Expand Up @@ -88,6 +88,7 @@ const auth = new Authenticator<User>(sessionStorage)
auth.use(
new RfdApiStrategy(
{
host: getRfdApiUrl(),
clientID: process.env.RFD_API_CLIENT_ID || '',
clientSecret: process.env.RFD_API_CLIENT_SECRET || '',
callbackURL: process.env.RFD_API_GOOGLE_CALLBACK_URL || '',
Expand All @@ -103,6 +104,7 @@ auth.use(
auth.use(
new RfdApiStrategy(
{
host: getRfdApiUrl(),
clientID: process.env.RFD_API_CLIENT_ID || '',
clientSecret: process.env.RFD_API_CLIENT_SECRET || '',
callbackURL: process.env.RFD_API_GITHUB_CALLBACK_URL || '',
Expand Down
12 changes: 11 additions & 1 deletion app/services/rfd.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import { generateAuthors, type Author } from '~/components/rfd/RfdPreview'
import { isTruthy } from '~/utils/isTruthy'
import { parseRfdNum } from '~/utils/parseRfdNum'
import { canUser } from '~/utils/permission'
import { can, Permission } from '~/utils/permission'

Check warning on line 18 in app/services/rfd.server.ts

View workflow job for this annotation

GitHub Actions / ci

Import "Permission" is only used as types
import type { GroupResponse, RfdListResponseItem, RfdResponse } from '~/utils/rfdApi'

import type { Group, User } from './authn.server'
Expand Down Expand Up @@ -55,6 +55,16 @@
const localRepo = process.env.LOCAL_RFD_REPO
export const isLocalMode = process.env.NODE_ENV === 'development' && localRepo

async function canUser(user: User, permission: Permission): Promise<boolean> {
const groups = (await fetchGroups(user)).filter((group) =>
user.groups.includes(group.name),
)
const allPermissions = user.permissions.concat(
groups.flatMap((group) => group.permissions),
)
return can(allPermissions, permission)
}

function findLineStartingWith(content: string, prefixRegex: string): string | undefined {
// (^|\n) is required to match either the first line (beginning of file) or
// subsequent lines
Expand Down
19 changes: 18 additions & 1 deletion app/services/rfdApi.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,23 @@
* Copyright Oxide Computer Company
*/

import { isLocalMode } from './rfd.server'

export function getRfdApiUrl(): string {
// If we are loading in local mode, then the API is not used, and it is fine to return
// and invalid value
if (isLocalMode) {
return ''
}

// Otherwise crash the system if we do not have an API target set
if (!process.env.RFD_API) {
throw Error('Env var RFD_API must be set when not running in local mode')
}

return process.env.RFD_API
}

export async function apiRequest<T>(
path: string,
accessToken: string | undefined,
Expand All @@ -18,7 +35,7 @@ export async function apiRequest<T>(
headers['Authorization'] = `Bearer ${accessToken}`
}

const url = `https://rfd-api.shared.oxide.computer/${path.replace(/^\//, '')}`
const url = `${getRfdApiUrl()}/${path.replace(/^\//, '')}`
console.info(`Requesting ${url} from the RFD API`)

const response = await fetch(url, { headers })
Expand Down
13 changes: 0 additions & 13 deletions app/utils/permission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,10 @@
* Copyright Oxide Computer Company
*/

import type { User } from '~/services/authn.server'
import { fetchGroups } from '~/services/rfd.server'

import type { RfdApiPermission } from './rfdApi'

export type Permission = { k: 'ReadDiscussions' } | { k: 'ReadRfd'; v: number }

export async function canUser(user: User, permission: Permission): Promise<boolean> {
const groups = (await fetchGroups(user)).filter((group) =>
user.groups.includes(group.name),
)
const allPermissions = user.permissions.concat(
groups.flatMap((group) => group.permissions),
)
return can(allPermissions, permission)
}

export function can(allPermissions: RfdApiPermission[], permission: Permission): boolean {
const checks = createChecks(permission)
const allowed = checks.some((check) => performCheck(allPermissions, check))
Expand Down
20 changes: 14 additions & 6 deletions app/utils/rfdApiStrategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
import type { RfdApiPermission, RfdApiProvider, RfdScope } from './rfdApi'

export type RfdApiStrategyOptions = {
host: string
clientID: string
clientSecret: string
callbackURL: string
Expand Down Expand Up @@ -85,11 +86,17 @@ export class RfdApiStrategy<User extends ExpiringUser> extends OAuth2Strategy<
RfdApiExtraParams
> {
public name = `rfd-api`

private readonly userInfoURL = 'https://rfd-api.shared.oxide.computer/self'
protected userInfoUrl = ``

constructor(
{ clientID, clientSecret, callbackURL, remoteProvider, scope }: RfdApiStrategyOptions,
{
host,
clientID,
clientSecret,
callbackURL,
remoteProvider,
scope,
}: RfdApiStrategyOptions,
verify: StrategyVerifyCallback<
User,
OAuth2StrategyVerifyParams<RfdApiProfile, RfdApiExtraParams>
Expand All @@ -100,13 +107,14 @@ export class RfdApiStrategy<User extends ExpiringUser> extends OAuth2Strategy<
clientID,
clientSecret,
callbackURL,
authorizationURL: `https://rfd-api.shared.oxide.computer/login/oauth/${remoteProvider}/code/authorize`,
tokenURL: `https://rfd-api.shared.oxide.computer/login/oauth/${remoteProvider}/code/token`,
authorizationURL: `${host}/login/oauth/${remoteProvider}/code/authorize`,
tokenURL: `${host}/login/oauth/${remoteProvider}/code/token`,
},
verify,
)
this.name = `${this.name}-${remoteProvider}`
this.scope = this.parseScope(scope)
this.userInfoUrl = `${host}/self`
}

protected authorizationParams(): URLSearchParams {
Expand All @@ -115,7 +123,7 @@ export class RfdApiStrategy<User extends ExpiringUser> extends OAuth2Strategy<
}

protected async userProfile(accessToken: string): Promise<RfdApiProfile> {
const response = await fetch(this.userInfoURL, {
const response = await fetch(this.userInfoUrl, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
Expand Down
Loading