Support secret changes and token become unauchorized (#2205)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2022-07-05 12:43:17 +07:00 committed by GitHub
parent 55e49257f8
commit 1f64039473
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 147 additions and 41 deletions

View File

@ -13,7 +13,7 @@
"docker:build": "docker build -t hardcoreeng/tool .", "docker:build": "docker build -t hardcoreeng/tool .",
"docker:staging": "../../common/scripts/docker_tag.sh hardcoreeng/tool staging", "docker:staging": "../../common/scripts/docker_tag.sh hardcoreeng/tool staging",
"docker:push": "../../common/scripts/docker_tag.sh hardcoreeng/tool", "docker:push": "../../common/scripts/docker_tag.sh hardcoreeng/tool",
"run-local": "cross-env MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin MINIO_ENDPOINT=localhost MONGO_URL=mongodb://localhost:27017 TRANSACTOR_URL=ws:/localhost:3333 TELEGRAM_DATABASE=telegram-service ELASTIC_URL=http://localhost:9200 REKONI_URL=http://localhost:4004 ts-node ./src/index.ts", "run-local": "cross-env SERVER_SECRET=secret MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin MINIO_ENDPOINT=localhost MONGO_URL=mongodb://localhost:27017 TRANSACTOR_URL=ws:/localhost:3333 TELEGRAM_DATABASE=telegram-service ELASTIC_URL=http://localhost:9200 REKONI_URL=http://localhost:4004 ts-node ./src/index.ts",
"upgrade": "rushx run-local upgrade", "upgrade": "rushx run-local upgrade",
"lint": "eslint src", "lint": "eslint src",
"format": "prettier --write src && eslint --fix src" "format": "prettier --write src && eslint --fix src"

View File

@ -30,7 +30,7 @@ import {
} from '@anticrm/account' } from '@anticrm/account'
import { setMetadata } from '@anticrm/platform' import { setMetadata } from '@anticrm/platform'
import { backup, backupList, createFileBackupStorage, createMinioBackupStorage, restore } from '@anticrm/server-backup' import { backup, backupList, createFileBackupStorage, createMinioBackupStorage, restore } from '@anticrm/server-backup'
import { decodeToken, generateToken } from '@anticrm/server-token' import serverToken, { decodeToken, generateToken } from '@anticrm/server-token'
import toolPlugin, { prepareTools, version } from '@anticrm/server-tool' import toolPlugin, { prepareTools, version } from '@anticrm/server-tool'
import { program } from 'commander' import { program } from 'commander'
import { Db, MongoClient } from 'mongodb' import { Db, MongoClient } from 'mongodb'
@ -46,6 +46,12 @@ import { diffWorkspace, dumpWorkspace, restoreWorkspace } from './workspace'
const { mongodbUri, minio } = prepareTools() const { mongodbUri, minio } = prepareTools()
const serverSecret = process.env.SERVER_SECRET
if (serverSecret === undefined) {
console.error('please provide server secret')
process.exit(1)
}
const transactorUrl = process.env.TRANSACTOR_URL const transactorUrl = process.env.TRANSACTOR_URL
if (transactorUrl === undefined) { if (transactorUrl === undefined) {
console.error('please provide transactor url.') console.error('please provide transactor url.')
@ -60,6 +66,7 @@ if (elasticUrl === undefined) {
setMetadata(toolPlugin.metadata.Endpoint, transactorUrl) setMetadata(toolPlugin.metadata.Endpoint, transactorUrl)
setMetadata(toolPlugin.metadata.Transactor, transactorUrl) setMetadata(toolPlugin.metadata.Transactor, transactorUrl)
setMetadata(serverToken.metadata.Secret, serverSecret)
async function withDatabase (uri: string, f: (db: Db, client: MongoClient) => Promise<any>): Promise<void> { async function withDatabase (uri: string, f: (db: Db, client: MongoClient) => Promise<any>): Promise<void> {
console.log(`connecting to database '${uri}'...`) console.log(`connecting to database '${uri}'...`)

View File

@ -73,6 +73,12 @@ export const OK = new Status(Severity.OK, platform.status.OK, {})
*/ */
export const ERROR = new Status(Severity.ERROR, platform.status.BadError, {}) export const ERROR = new Status(Severity.ERROR, platform.status.BadError, {})
/**
* Error Status for Unauthorized
* @public
*/
export const UNAUTHORIZED = new Status(Severity.ERROR, platform.status.Unauthorized, {})
/** /**
* @public * @public
* @param message - * @param message -

View File

@ -22,9 +22,20 @@ export async function connect (title: string): Promise<Client | undefined> {
} }
const getClient = await getResource(client.function.GetClient) const getClient = await getResource(client.function.GetClient)
const instance = await getClient(token, endpoint, () => { const instance = await getClient(
token,
endpoint,
() => {
location.reload() location.reload()
},
() => {
clearMetadata()
navigate({
path: [login.component.LoginApp],
query: {}
}) })
}
)
console.log('logging in as', email) console.log('logging in as', email)
const me = await instance.findOne(contact.class.EmployeeAccount, { email }) const me = await instance.findOne(contact.class.EmployeeAccount, { email })
@ -33,10 +44,7 @@ export async function connect (title: string): Promise<Client | undefined> {
setCurrentAccount(me) setCurrentAccount(me)
} else { } else {
console.error('WARNING: no employee account found.') console.error('WARNING: no employee account found.')
setMetadataLocalStorage(login.metadata.LoginToken, null) clearMetadata()
setMetadataLocalStorage(login.metadata.LoginEndpoint, null)
setMetadataLocalStorage(login.metadata.LoginEmail, null)
setMetadataLocalStorage(login.metadata.CurrentWorkspace, null)
navigate({ navigate({
path: [login.component.LoginApp], path: [login.component.LoginApp],
query: { navigateUrl: encodeURIComponent(JSON.stringify(getCurrentLocation())) } query: { navigateUrl: encodeURIComponent(JSON.stringify(getCurrentLocation())) }
@ -73,3 +81,9 @@ export async function connect (title: string): Promise<Client | undefined> {
return instance return instance
} }
function clearMetadata (): void {
setMetadataLocalStorage(login.metadata.LoginToken, null)
setMetadataLocalStorage(login.metadata.LoginEndpoint, null)
setMetadataLocalStorage(login.metadata.LoginEmail, null)
setMetadataLocalStorage(login.metadata.CurrentWorkspace, null)
}

View File

@ -30,7 +30,7 @@ import type {
TxResult TxResult
} from '@anticrm/core' } from '@anticrm/core'
import core from '@anticrm/core' import core from '@anticrm/core'
import { getMetadata, PlatformError, readResponse, ReqId, serialize } from '@anticrm/platform' import { getMetadata, PlatformError, readResponse, ReqId, serialize, UNAUTHORIZED } from '@anticrm/platform'
class DeferredPromise { class DeferredPromise {
readonly promise: Promise<any> readonly promise: Promise<any>
@ -53,7 +53,8 @@ class Connection implements ClientConnection {
constructor ( constructor (
private readonly url: string, private readonly url: string,
private readonly handler: TxHander, private readonly handler: TxHander,
private readonly onUpgrade?: () => void private readonly onUpgrade?: () => void,
private readonly onUnauthorized?: () => void
) { ) {
console.log('connection created') console.log('connection created')
this.interval = setInterval(() => { this.interval = setInterval(() => {
@ -72,7 +73,11 @@ class Connection implements ClientConnection {
try { try {
return await this.openConnection() return await this.openConnection()
} catch (err: any) { } catch (err: any) {
console.log('failed to connect') console.log('failed to connect', err)
if (err.code === UNAUTHORIZED.code) {
this.onUnauthorized?.()
throw err
}
await new Promise((resolve) => { await new Promise((resolve) => {
setTimeout(() => { setTimeout(() => {
@ -93,6 +98,10 @@ class Connection implements ClientConnection {
websocket.onmessage = (event: MessageEvent) => { websocket.onmessage = (event: MessageEvent) => {
const resp = readResponse(event.data) const resp = readResponse(event.data)
if (resp.id === -1 && resp.result === 'hello') { if (resp.id === -1 && resp.result === 'hello') {
if (resp.error !== undefined) {
reject(resp.error)
return
}
resolve(websocket) resolve(websocket)
return return
} }
@ -191,6 +200,11 @@ class Connection implements ClientConnection {
/** /**
* @public * @public
*/ */
export async function connect (url: string, handler: TxHander, onUpgrade?: () => void): Promise<ClientConnection> { export async function connect (
return new Connection(url, handler, onUpgrade) url: string,
handler: TxHander,
onUpgrade?: () => void,
onUnauthorized?: () => void
): Promise<ClientConnection> {
return new Connection(url, handler, onUpgrade, onUnauthorized)
} }

View File

@ -32,7 +32,12 @@ export default async () => {
return { return {
function: { function: {
GetClient: async (token: string, endpoint: string, onUpgrade?: () => void): Promise<Client> => { GetClient: async (
token: string,
endpoint: string,
onUpgrade?: () => void,
onUnauthorized?: () => void
): Promise<Client> => {
if (token !== _token && client !== undefined) { if (token !== _token && client !== undefined) {
await client.close() await client.close()
client = undefined client = undefined
@ -43,7 +48,7 @@ export default async () => {
(handler: TxHander) => { (handler: TxHander) => {
const url = new URL(`/${token}`, endpoint) const url = new URL(`/${token}`, endpoint)
console.log('connecting to', url.href) console.log('connecting to', url.href)
return connect(url.href, handler, onUpgrade) return connect(url.href, handler, onUpgrade, onUnauthorized)
}, },
filterModel ? getPlugins() : undefined filterModel ? getPlugins() : undefined
) )

View File

@ -52,7 +52,12 @@ export interface ClientSocket {
/** /**
* @public * @public
*/ */
export type ClientFactory = (token: string, endpoint: string, onUpgrade?: () => void) => Promise<Client> export type ClientFactory = (
token: string,
endpoint: string,
onUpgrade?: () => void,
onUnauthorized?: () => void
) => Promise<Client>
export default plugin(clientId, { export default plugin(clientId, {
metadata: { metadata: {

View File

@ -14,7 +14,7 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { fetchMetadataLocalStorage, location, Popup } from '@anticrm/ui' import { fetchMetadataLocalStorage, location, Popup, ticker } from '@anticrm/ui'
import LoginForm from './LoginForm.svelte' import LoginForm from './LoginForm.svelte'
import SignupForm from './SignupForm.svelte' import SignupForm from './SignupForm.svelte'
@ -28,7 +28,10 @@
let navigateUrl: string | undefined let navigateUrl: string | undefined
const token = fetchMetadataLocalStorage(login.metadata.LoginToken) function getToken (timer: number): string | null {
return fetchMetadataLocalStorage(login.metadata.LoginToken)
}
$: token = getToken($ticker)
onDestroy( onDestroy(
location.subscribe(async (loc) => { location.subscribe(async (loc) => {

View File

@ -18,7 +18,7 @@
import { Button, getCurrentLocation, Label, navigate, setMetadataLocalStorage } from '@anticrm/ui' import { Button, getCurrentLocation, Label, navigate, setMetadataLocalStorage } from '@anticrm/ui'
import { workbenchId } from '@anticrm/workbench' import { workbenchId } from '@anticrm/workbench'
import login from '../plugin' import login from '../plugin'
import { getWorkspaces, selectWorkspace } from '../utils' import { getWorkspaces, selectWorkspace, Workspace } from '../utils'
import StatusControl from './StatusControl.svelte' import StatusControl from './StatusControl.svelte'
export let navigateUrl: string | undefined = undefined export let navigateUrl: string | undefined = undefined
@ -44,6 +44,19 @@
} }
} }
async function _getWorkspaces (): Promise<Workspace[]> {
try {
return getWorkspaces()
} catch (err: any) {
setMetadataLocalStorage(login.metadata.LoginToken, null)
setMetadataLocalStorage(login.metadata.LoginEndpoint, null)
setMetadataLocalStorage(login.metadata.LoginEmail, null)
setMetadataLocalStorage(login.metadata.CurrentWorkspace, null)
changeAccount()
throw err
}
}
function createWorkspace (): void { function createWorkspace (): void {
const loc = getCurrentLocation() const loc = getCurrentLocation()
loc.path[1] = 'createWorkspace' loc.path[1] = 'createWorkspace'
@ -65,7 +78,7 @@
<div class="status"> <div class="status">
<StatusControl {status} /> <StatusControl {status} />
</div> </div>
{#await getWorkspaces() then workspaces} {#await _getWorkspaces() then workspaces}
<div class="form"> <div class="form">
{#each workspaces as workspace} {#each workspaces as workspace}
<div <div

View File

@ -13,8 +13,17 @@
// limitations under the License. // limitations under the License.
// //
import type { Request, Response } from '@anticrm/platform' import {
import { getMetadata, OK, serialize, Status, unknownError, unknownStatus } from '@anticrm/platform' PlatformError,
Request,
Response,
getMetadata,
OK,
serialize,
Status,
unknownError,
unknownStatus
} from '@anticrm/platform'
import login from '@anticrm/login' import login from '@anticrm/login'
import { fetchMetadataLocalStorage, getCurrentLocation, navigate } from '@anticrm/ui' import { fetchMetadataLocalStorage, getCurrentLocation, navigate } from '@anticrm/ui'
@ -204,6 +213,9 @@ export async function getWorkspaces (): Promise<Workspace[]> {
}) })
const result: Response<any> = await response.json() const result: Response<any> = await response.json()
console.log(result) console.log(result)
if (result.error != null) {
throw new PlatformError(result.error)
}
return result.result return result.result
} catch (err) { } catch (err) {
return [] return []

View File

@ -14,30 +14,30 @@
// limitations under the License. // limitations under the License.
// //
import { readResponse, serialize } from '@anticrm/platform' import { readResponse, serialize, UNAUTHORIZED } from '@anticrm/platform'
import { start, disableLogging } from '../server'
import { generateToken } from '@anticrm/server-token' import { generateToken } from '@anticrm/server-token'
import WebSocket from 'ws' import WebSocket from 'ws'
import { disableLogging, start } from '../server'
import { import {
Doc,
Ref,
Class, Class,
Doc,
DocumentQuery, DocumentQuery,
Domain,
FindOptions, FindOptions,
FindResult, FindResult,
Tx,
TxResult,
ModelDb,
MeasureMetricsContext,
toFindResult,
Hierarchy, Hierarchy,
MeasureMetricsContext,
ModelDb,
Ref,
ServerStorage, ServerStorage,
Domain toFindResult,
Tx,
TxResult
} from '@anticrm/core' } from '@anticrm/core'
import { SessionContext } from '@anticrm/server-core' import { SessionContext } from '@anticrm/server-core'
import { genMinModel } from './minmodel'
import { ClientSession } from '../client' import { ClientSession } from '../client'
import { genMinModel } from './minmodel'
describe('server', () => { describe('server', () => {
disableLogging() disableLogging()
@ -101,6 +101,12 @@ describe('server', () => {
conn.on('error', () => { conn.on('error', () => {
conn.close() conn.close()
}) })
conn.on('message', (msg: string) => {
const resp = readResponse(msg)
expect(resp.result === 'hello')
expect(resp.error?.code).toBe(UNAUTHORIZED.code)
conn.close()
})
conn.on('close', () => { conn.on('close', () => {
done() done()
}) })

View File

@ -14,7 +14,7 @@
// //
import core, { MeasureContext, Ref, Space, TxFactory } from '@anticrm/core' import core, { MeasureContext, Ref, Space, TxFactory } from '@anticrm/core'
import { readRequest, Response, serialize, unknownError } from '@anticrm/platform' import { readRequest, Response, serialize, UNAUTHORIZED, unknownError } from '@anticrm/platform'
import type { Pipeline } from '@anticrm/server-core' import type { Pipeline } from '@anticrm/server-core'
import { decodeToken, Token } from '@anticrm/server-token' import { decodeToken, Token } from '@anticrm/server-token'
import { createServer, IncomingMessage } from 'http' import { createServer, IncomingMessage } from 'http'
@ -263,9 +263,20 @@ export function start (
console.log('client connected with payload', payload) console.log('client connected with payload', payload)
wss.handleUpgrade(request, socket, head, (ws) => wss.emit('connection', ws, request, payload)) wss.handleUpgrade(request, socket, head, (ws) => wss.emit('connection', ws, request, payload))
} catch (err) { } catch (err) {
console.log('unauthorized client') wss.handleUpgrade(request, socket, head, (ws) => {
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n') const resp: Response<any> = {
socket.destroy() id: -1,
error: UNAUTHORIZED,
result: 'hello'
}
ws.send(serialize(resp))
ws.onmessage = (msg) => {
const resp: Response<any> = {
error: UNAUTHORIZED
}
ws.send(serialize(resp))
}
})
} }
}) })

View File

@ -0,0 +1,8 @@
#!/bin/bash
# Usage
# ./install-elastic-plugin-setup.sh sanity_elastic_1
curdir=$(dirname $0)
${curdir}/install-elastic-plugin.sh $@
${curdir}/setup-elastic.sh 9200

View File

@ -3,7 +3,7 @@
docker-compose -p sanity kill docker-compose -p sanity kill
docker-compose -p sanity down --volumes docker-compose -p sanity down --volumes
docker-compose -p sanity up -d --force-recreate --renew-anon-volumes docker-compose -p sanity up -d --force-recreate --renew-anon-volumes
./setup-elastic.sh ./setup-elastic.sh 9201
# Creae workspace record in accounts # Creae workspace record in accounts
./tool.sh create-workspace sanity-ws -o SanityTest ./tool.sh create-workspace sanity-ws -o SanityTest

View File

@ -1,8 +1,9 @@
res='' res=''
echo "Warning Elastic to up and running with attachment processor..." port=$1
echo "Warning Elastic to up and running with attachment processor... ${port}"
while true while true
do do
res=$(curl -s -XPUT "localhost:9201/_ingest/pipeline/attachment?pretty" -H 'Content-Type: application/json' -d' res=$(curl -s -XPUT "localhost:${port}/_ingest/pipeline/attachment?pretty" -H 'Content-Type: application/json' -d'
{ {
"description" : "Field for processing file attachments", "description" : "Field for processing file attachments",
"processors" : [ "processors" : [

View File

@ -6,5 +6,6 @@ export MINIO_ENDPOINT=localhost:9002
export MONGO_URL=mongodb://localhost:27018 export MONGO_URL=mongodb://localhost:27018
export TRANSACTOR_URL=ws:/localhost:3334 export TRANSACTOR_URL=ws:/localhost:3334
export ELASTIC_URL=http://localhost:9201 export ELASTIC_URL=http://localhost:9201
export SERVER_SECRET=secret
node ../dev/tool/bundle.js $@ node ../dev/tool/bundle.js $@