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

Refactor StatefulAuthProvider for static initialization and improved session management #1822

Merged
merged 40 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
c93a2b4
Work in progress
pastuxso Nov 20, 2024
0d1db50
Creating singleton
pastuxso Nov 20, 2024
84d07f4
Singletonization
pastuxso Nov 21, 2024
ae58c29
Sync repo
pastuxso Nov 21, 2024
db175c3
Merge remote-tracking branch 'origin/main' into cris/stateful-auth-pr…
pastuxso Nov 25, 2024
0a0a6ec
Sync tests
pastuxso Nov 25, 2024
149769b
Merge branch 'main' into cris/stateful-auth-provider
pastuxso Nov 26, 2024
0db6287
Reapply "Add sign-out event handling and panel HTML refresh for Cloud…
pastuxso Nov 26, 2024
10a48d6
Add AuthSessionChangeHandler to prevent multiple listener calls
pastuxso Nov 26, 2024
d4bed15
Merge branch 'main' into cris/stateful-auth-provider
pastuxso Nov 27, 2024
a22b762
Passing tests
pastuxso Nov 27, 2024
9e5ea21
Remove internal usage of the context from the AuthSessionChangeHandler
pastuxso Nov 27, 2024
3eb802a
Merge branch 'main' into cris/stateful-auth-provider
pastuxso Dec 2, 2024
55df0cd
Branch sync
pastuxso Dec 2, 2024
c9d4062
Merge branch 'main' into cris/stateful-auth-provider
pastuxso Dec 3, 2024
0f09e60
Merge remote-tracking branch 'origin/main' into cris/stateful-auth-pr…
pastuxso Dec 3, 2024
590bce3
Removing kernel and RunmeUriHandler dependencies.
pastuxso Dec 4, 2024
111b6bf
Decrease debounce time
pastuxso Dec 5, 2024
b583fc8
Silently asks If there is a session for CloudPanel
pastuxso Dec 5, 2024
f76c179
No more silence on error
pastuxso Dec 5, 2024
460f02e
COde cleanup
pastuxso Dec 5, 2024
68e81cb
Removes unnecessary code
pastuxso Dec 5, 2024
ea2c9c9
Implementing new argument for silent token
pastuxso Dec 5, 2024
d2040b1
Rollback reactive approach, it’s not working
pastuxso Dec 5, 2024
41d491e
Test failling
pastuxso Dec 5, 2024
822dad5
Rollback panel getAppToken
pastuxso Dec 5, 2024
5a8d9ae
Passing tests
pastuxso Dec 5, 2024
f85c53f
Cleanup
pastuxso Dec 5, 2024
a69469a
Merge branch 'main' into cris/stateful-auth-provider
pastuxso Dec 10, 2024
45cc8c4
Minor variable change
pastuxso Dec 10, 2024
2c08fb1
Merge branch 'main' into cris/stateful-auth-provider
pastuxso Dec 11, 2024
ed56592
Renamed `getSession` to `currentSession` for better clarity.
pastuxso Dec 11, 2024
0c50c6c
Remove token retrieval from `getHydratedHtml` to prevent double authe…
pastuxso Dec 11, 2024
190ffc2
repository sync
pastuxso Dec 11, 2024
5279640
Remove getPlatformAuthSession function
pastuxso Dec 11, 2024
6af3091
Updates tests
pastuxso Dec 11, 2024
eacf42d
Minor fix
pastuxso Dec 11, 2024
3b7f6fe
Emit `signIn` event to update panel when login is triggered externally
pastuxso Dec 12, 2024
41ed6f5
Use cloud over platform
sourishkrout Dec 12, 2024
90d09ad
Fix tests
sourishkrout Dec 12, 2024
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
2 changes: 1 addition & 1 deletion .github/scripts/overwrites/stateful.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ npm pkg set name="$EXTENSION_NAME"
npm pkg set displayName="Stateful Notebooks for DevOps"
npm pkg set description="DevOps Notebooks built on Runme, connected for collaboration."
npm pkg set homepage="https://stateful.com"
npm pkg set contributes.configuration[0].properties[runme.app.baseDomain].default="platform.stateful.com"
npm pkg set contributes.configuration[0].properties[runme.app.baseDomain].default="cloud.stateful.com"
npm pkg set contributes.configuration[0].properties[runme.app.platformAuth].default=true --json
npm pkg set contributes.configuration[0].properties[runme.server.lifecycleIdentity].default=1 --json
npm pkg set contributes.configuration[0].properties[runme.app.notebookAutoSave].default="yes"
Expand Down
2 changes: 1 addition & 1 deletion README-platform.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ The Stateful cloud offers a suite of tools and services designed to enhance your

## Getting Started

1. **Sign Up**: Create your free account at [Stateful](https://platform.stateful.com/).
1. **Sign Up**: Create your free account at [Stateful](https://cloud.stateful.com/).
2. **Install VS Code Extension**: Download the Stateful extension from the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=stateful.platform). The extension is fully compatible with Runme, but adds authentication, collaboration, and security features, making it secure for teams and inside companies.
3. **Explore**: Start creating, running, sharing, and discussing your first DevOps Notebook and its commands using the Stateful cloud.

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -992,7 +992,7 @@
},
"runme.app.baseDomain": {
"type": "string",
"default": "platform.stateful.com",
"default": "cloud.stateful.com",
"scope": "window",
"markdownDescription": "Base domain to be use for Runme app"
},
Expand Down
21 changes: 13 additions & 8 deletions src/extension/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,26 @@ import { Uri } from 'vscode'

import { getRunmeAppUrl } from '../../utils/configuration'
import { getFeaturesContext } from '../features'
import { StatefulAuthProvider } from '../provider/statefulAuth'

export function InitializeClient({
uri,
runmeToken,
}: {
uri?: string | undefined
runmeToken: string
}) {
export async function InitializeCloudClient(uri?: string) {
const session = await StatefulAuthProvider.instance.currentSession()

if (!session) {
throw new Error('You must authenticate with your Stateful account')
}

return InitializeClient({ uri, token: session.accessToken })
}

function InitializeClient({ uri, token }: { uri?: string | undefined; token: string }) {
const authLink = setContext((_, { headers }) => {
const context = getFeaturesContext()
return {
headers: {
...headers,
'Auth-Provider': 'platform',
authorization: runmeToken ? `Bearer ${runmeToken}` : '',
authorization: token ? `Bearer ${token}` : '',
'X-Extension-Id': context?.extensionId,
'X-Extension-Os': context?.os,
'X-Extension-Version': context?.extensionVersion,
Expand Down
74 changes: 74 additions & 0 deletions src/extension/authSessionChangeHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Subject, Subscription } from 'rxjs'
import { debounceTime, distinctUntilChanged } from 'rxjs/operators'
import { authentication, AuthenticationSessionsChangeEvent, Disposable } from 'vscode'

export default class AuthSessionChangeHandler implements Disposable {
static #instance: AuthSessionChangeHandler | null = null

#disposables: Disposable[] = []
#eventSubject: Subject<AuthenticationSessionsChangeEvent>
#subscriptions: Subscription[] = []
#listeners: ((event: AuthenticationSessionsChangeEvent) => void)[] = []

private constructor(private debounceTimeMs: number = 100) {
this.#eventSubject = new Subject<AuthenticationSessionsChangeEvent>()
this.#subscriptions.push(
this.#eventSubject
.pipe(distinctUntilChanged(this.eventComparer), debounceTime(this.debounceTimeMs))
.subscribe((event) => {
this.notifyListeners(event)
}),
)

this.#disposables.push(
authentication.onDidChangeSessions((e) => {
this.#eventSubject.next(e)
}),
)
}

public static get instance(): AuthSessionChangeHandler {
if (!this.#instance) {
this.#instance = new AuthSessionChangeHandler()
}

return this.#instance
}

public addListener(listener: (event: AuthenticationSessionsChangeEvent) => void): void {
this.#listeners.push(listener)
}

public removeListener(listener: (event: AuthenticationSessionsChangeEvent) => void): void {
this.#listeners = this.#listeners.filter((l) => l !== listener)
}

private notifyListeners(event: AuthenticationSessionsChangeEvent): void {
for (const listener of this.#listeners) {
try {
listener(event)
} catch (err) {
console.error('Error in listener:', err)
}
}
}

private eventComparer(
previous: AuthenticationSessionsChangeEvent,
current: AuthenticationSessionsChangeEvent,
): boolean {
return (
previous.provider.id === current.provider.id &&
JSON.stringify(previous) === JSON.stringify(current)
)
}

public async dispose() {
this.#disposables.forEach((d) => d.dispose())
this.#subscriptions = []
this.#eventSubject.complete()
this.#listeners = []

AuthSessionChangeHandler.#instance = null
}
}
6 changes: 2 additions & 4 deletions src/extension/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import { Kernel } from '../kernel'
import {
getAnnotations,
getNotebookCategories,
getPlatformAuthSession,
getTerminalByCell,
openFileAsRunmeNotebook,
promptUserSession,
Expand All @@ -52,7 +51,7 @@ import {
} from '../../constants'
import ContextState from '../contextState'
import { createGist } from '../services/github/gist'
import { InitializeClient } from '../api/client'
import { InitializeCloudClient } from '../api/client'
import { GetUserEnvironmentsDocument } from '../__generated-platform__/graphql'
import { EnvironmentManager } from '../environment/manager'
import features from '../features'
Expand Down Expand Up @@ -562,8 +561,7 @@ export async function createCellGistCommand(cell: NotebookCell, context: Extensi
}

export async function selectEnvironment(manager: EnvironmentManager) {
const session = await getPlatformAuthSession()
const graphClient = InitializeClient({ runmeToken: session?.accessToken! })
const graphClient = await InitializeCloudClient()

const result = await graphClient.query({
query: GetUserEnvironmentsDocument,
Expand Down
77 changes: 23 additions & 54 deletions src/extension/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
env,
Uri,
NotebookCell,
authentication,
} from 'vscode'
import { TelemetryReporter } from 'vscode-telemetry'
import Channel from 'tangle/webviews'
Expand Down Expand Up @@ -41,8 +40,6 @@ import {
getDefaultWorkspace,
bootFile,
resetNotebookSettings,
getPlatformAuthSession,
getGithubAuthSession,
openFileAsRunmeNotebook,
} from './utils'
import { RunmeTaskProvider } from './provider/runmeTask'
Expand Down Expand Up @@ -95,6 +92,7 @@ import { EnvironmentManager } from './environment/manager'
import ContextState from './contextState'
import { RunmeIdentity } from './grpc/serializerTypes'
import * as features from './features'
import AuthSessionChangeHandler from './authSessionChangeHandler'

export class RunmeExtension {
protected serializer?: SerializerBase
Expand Down Expand Up @@ -218,7 +216,6 @@ export class RunmeExtension {
// extension is deactivated.
context.subscriptions.push(aiManager)

const uriHandler = new RunmeUriHandler(context, kernel, getForceNewWindowConfig())
const winCodeLensRunSurvey = new survey.SurveyWinCodeLensRun(context)
const surveys: Disposable[] = [
winCodeLensRunSurvey,
Expand Down Expand Up @@ -260,6 +257,8 @@ export class RunmeExtension {
serializer,
server,
treeViewer,
StatefulAuthProvider.instance,
AuthSessionChangeHandler.instance,
...this.registerPanels(kernel, context),
...surveys,
workspace.registerNotebookSerializer(Kernel.type, serializer, {
Expand Down Expand Up @@ -338,7 +337,7 @@ export class RunmeExtension {
/**
* Uri handler
*/
window.registerUriHandler(uriHandler),
window.registerUriHandler(new RunmeUriHandler(context, kernel, getForceNewWindowConfig())),

/**
* Runme Message Display commands
Expand Down Expand Up @@ -398,7 +397,7 @@ export class RunmeExtension {
commands.executeCommand('runme.lifecycleIdentitySelection', RunmeIdentity.CELL),
),

RunmeExtension.registerCommand(
commands.registerCommand(
'runme.lifecycleIdentitySelection',
async (identity?: RunmeIdentity) => {
if (identity === undefined) {
Expand All @@ -412,6 +411,10 @@ export class RunmeExtension {
return
}

TelemetryReporter.sendTelemetryEvent('extension.command', {
command: 'runme.lifecycleIdentitySelection',
})

await ContextState.addKey(NOTEBOOK_LIFECYCLE_ID, identity)

await Promise.all(
Expand Down Expand Up @@ -486,71 +489,37 @@ export class RunmeExtension {
}

if (kernel.isFeatureOn(FeatureName.RequireStatefulAuth)) {
const statefulAuthProvider = new StatefulAuthProvider(context, uriHandler)
context.subscriptions.push(statefulAuthProvider)

const session = await getPlatformAuthSession(false, true)
let sessionFromToken = false
if (!session) {
sessionFromToken = await statefulAuthProvider.bootstrapFromToken()
}

const forceLogin = kernel.isFeatureOn(FeatureName.ForceLogin) || sessionFromToken
const silent = forceLogin ? undefined : true

getPlatformAuthSession(forceLogin, silent)
.then((session) => {
if (session) {
statefulAuthProvider.showLoginNotification()
}
})
.catch((error) => {
let message
if (error instanceof Error) {
message = error.message
} else {
message = JSON.stringify(error)
}

// https://github.com/microsoft/vscode/blob/main/src/vs/workbench/api/browser/mainThreadAuthentication.ts#L238
// throw new Error('User did not consent to login.')
// Calling again to ensure User Menu Badge
if (forceLogin && message === 'User did not consent to login.') {
getPlatformAuthSession(false)
}
})
await StatefulAuthProvider.instance.ensureSession()
}

if (kernel.isFeatureOn(FeatureName.Gist)) {
context.subscriptions.push(new GithubAuthProvider(context))
getGithubAuthSession(false).then((session) => {
kernel.updateFeatureContext('githubAuth', !!session)
})
context.subscriptions.push(new GithubAuthProvider(context, kernel))
}

authentication.onDidChangeSessions((e) => {
AuthSessionChangeHandler.instance.addListener((e) => {
if (
StatefulAuthProvider.instance &&
kernel.isFeatureOn(FeatureName.RequireStatefulAuth) &&
e.provider.id === AuthenticationProviders.Stateful
) {
getPlatformAuthSession(false, true).then(async (session) => {
if (!!session) {
StatefulAuthProvider.instance.currentSession().then(async (session) => {
if (session) {
await commands.executeCommand('runme.lifecycleIdentitySelection', RunmeIdentity.ALL)
kernel.emitPanelEvent('runme.cloud', 'onCommand', {
name: 'signIn',
panelId: 'runme.cloud',
})
} else {
const settingsDefault = getServerLifecycleIdentity()
await commands.executeCommand('runme.lifecycleIdentitySelection', settingsDefault)
kernel.emitPanelEvent('runme.cloud', 'onCommand', {
name: 'signOut',
panelId: 'runme.cloud',
})
}
kernel.updateFeatureContext('statefulAuth', !!session)
})
}
if (
kernel.isFeatureOn(FeatureName.Gist) &&
e.provider.id === AuthenticationProviders.GitHub
) {
getGithubAuthSession(false).then((session) => {
kernel.updateFeatureContext('githubAuth', !!session)
})
}
})

// only ever enabled in hosted playground
Expand Down
6 changes: 2 additions & 4 deletions src/extension/handler/uri.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
TaskScope,
ShellExecution,
tasks,
EventEmitter,
Disposable,
} from 'vscode'
import got from 'got'
Expand All @@ -23,6 +22,7 @@ import { TelemetryReporter } from 'vscode-telemetry'
import getLogger from '../logger'
import { Kernel } from '../kernel'
import { AuthenticationProviders } from '../../constants'
import { StatefulAuthProvider } from '../provider/statefulAuth'

import {
getProjectDir,
Expand All @@ -45,8 +45,6 @@ const extensionNames: { [key: string]: string } = {

export class RunmeUriHandler implements UriHandler, Disposable {
#disposables: Disposable[] = []
readonly #onAuth = this.register(new EventEmitter<Uri>())
readonly onAuthEvent = this.#onAuth.event

constructor(
private context: ExtensionContext,
Expand All @@ -70,7 +68,7 @@ export class RunmeUriHandler implements UriHandler, Disposable {
command,
type: AuthenticationProviders.Stateful,
})
this.#onAuth.fire(uri)
StatefulAuthProvider.instance.fireOnAuthEvent(uri)
return
} else if (command === 'setup') {
const { fileToOpen, repository } = parseParams(params)
Expand Down
2 changes: 2 additions & 0 deletions src/extension/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { TelemetryReporter } from 'vscode-telemetry'
import { RunmeExtension } from './extension'
import getLogger from './logger'
import { isTelemetryEnabled } from './utils'
import { StatefulAuthProvider } from './provider/statefulAuth'

declare const CONNECTION_STR: string

Expand Down Expand Up @@ -33,6 +34,7 @@ export async function activate(context: ExtensionContext) {

log.info('Activating Extension')
try {
StatefulAuthProvider.initialize(context)
await ext.initialize(context)
log.info('Extension successfully activated')
} catch (err: any) {
Expand Down
Loading
Loading