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

🪲 [Fix]: Update docs #50

Merged
merged 11 commits into from
Nov 21, 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
215 changes: 180 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,177 @@ we aim to store this data using a the concept of `Contexts` that are stored loca
developers separate user and module data from the module code, so that modules can be created in a way where users can resume from where they left off
without having to reconfigure the module or log in to services that support refreshing sessions with data you can store, i.e., refresh tokens.

## What is a `Context`?

The consept of `Contexts` is built on top of the functionality provided by the `Microsoft.PowerShell.SecretManagement` and
`Microsoft.PowerShell.SecretStore` modules. The `Context` module manages a set of `secrets` that is stored in a `SecretVault` instance. A context in
this case is a collection of secrets and data that is combined to represent a context for a module or a user.

## What is a `Context`?
this case is a data structure that supports secrets and regular datatypes converted to a modified JSON structure and stored as a string based secret
in the `SecretStore`. The `Context` is stored in the `SecretVault` as a secret with the name `Context:<ContextId>`.

A `Context` is collection of a name, data and secrets. A context must always have a name and the type of data you can store is:
The context is stored as compressed JSON and could look something like the examples below. These are the same data but one shows the JSON structure
that is stored in the `SecretStore` and the other shows the same data as a `PSCustomObject` that could be used in a PowerShell script.

- Byte[]
- String
- SecureString
- PSCredential
- Hashtable
<details>
<summary>PSCustomObject</summary>

The context is stored as hashtable and could look something like this:
Typical the first input to a context (altho it can also be a hashtable or any other object type that converts with JSON)

```pwsh
@{
Name = "GitHub" # Required: Used to store the context in the vault.
AccessToken = "123456",
AccessTokenExpirationDate = '2021-12-31T23:59:59'
RefreshToken = '654321'
RefreshTokenExpirationDate = '2021-12-31T23:59:59'
APIVersion = 'v3'
APIHost = 'https://api.github.com'
ClientId = '123456'
Scope = 'repo, user'
[PSCustomObject]@{
Username = 'john_doe'
AuthToken = 'ghp_12345ABCDE67890FGHIJ' | ConvertTo-SecureString -AsPlainText -Force #gitleaks:allow
LoginTime = Get-Date
IsTwoFactorAuth = $true
TwoFactorMethods = @('TOTP', 'SMS')
LastLoginAttempts = @(
[PSCustomObject]@{
Timestamp = (Get-Date).AddHours(-1)
IP = '192.168.1.101' | ConvertTo-SecureString -AsPlainText -Force
Success = $true
},
[PSCustomObject]@{
Timestamp = (Get-Date).AddDays(-1)
IP = '203.0.113.5' | ConvertTo-SecureString -AsPlainText -Force
Success = $false
}
)
UserPreferences = @{
Theme = 'dark'
DefaultBranch = 'main'
Notifications = [PSCustomObject]@{
Email = $true
Push = $false
SMS = $true
}
CodeReview = @('PR Comments', 'Inline Suggestions')
}
Repositories = @(
[PSCustomObject]@{
Name = 'Repo1'
IsPrivate = $true
CreatedDate = (Get-Date).AddMonths(-6)
Stars = 42
Languages = @('Python', 'JavaScript')
},
[PSCustomObject]@{
Name = 'Repo2'
IsPrivate = $false
CreatedDate = (Get-Date).AddYears(-1)
Stars = 130
Languages = @('C#', 'HTML', 'CSS')
}
)
AccessScopes = @('repo', 'user', 'gist', 'admin:org')
ApiRateLimits = [PSCustomObject]@{
Limit = 5000
Remaining = 4985
ResetTime = (Get-Date).AddMinutes(30)
}
SessionMetaData = [PSCustomObject]@{
SessionID = 'sess_abc123'
Device = 'Windows-PC'
Location = [PSCustomObject]@{
Country = 'USA'
City = 'New York'
}
BrowserInfo = [PSCustomObject]@{
Name = 'Chrome'
Version = '118.0.1'
}
}
}
```
</details>

<details>
<summary>JSON</summary>

This is same as what is stored, except that this is an uncomressed version for readability.

```json
{
"Username": "john_doe",
"AuthToken": "[SECURESTRING]ghp_12345ABCDE67890FGHIJ",
"LoginTime": "2024-11-21T21:16:56.2518249+01:00",
"IsTwoFactorAuth": true,
"TwoFactorMethods": [
"TOTP",
"SMS"
],
"LastLoginAttempts": [
{
"Timestamp": "2024-11-21T20:16:56.2518510+01:00",
"IP": "[SECURESTRING]192.168.1.101",
"Success": true
},
{
"Timestamp": "2024-11-20T21:16:56.2529436+01:00",
"IP": "[SECURESTRING]203.0.113.5",
"Success": false
}
],
"UserPreferences": {
"Theme": "dark",
"DefaultBranch": "main",
"Notifications": {
"Email": true,
"Push": false,
"SMS": true
},
"CodeReview": [
"PR Comments",
"Inline Suggestions"
]
},
"Repositories": [
{
"Name": "Repo1",
"IsPrivate": true,
"CreatedDate": "2024-05-21T21:16:56.2540703+02:00",
"Stars": 42,
"Languages": [
"Python",
"JavaScript"
]
},
{
"Name": "Repo2",
"IsPrivate": false,
"CreatedDate": "2023-11-21T21:16:56.2545789+01:00",
"Stars": 130,
"Languages": [
"C#",
"HTML",
"CSS"
]
}
],
"AccessScopes": [
"repo",
"user",
"gist",
"admin:org"
],
"ApiRateLimits": {
"Limit": 5000,
"Remaining": 4985,
"ResetTime": "2024-11-21T21:46:56.2550348+01:00"
},
"SessionMetaData": {
"SessionID": "sess_abc123",
"Device": "Windows-PC",
"Location": {
"Country": "USA",
"City": "New York"
},
"BrowserInfo": {
"Name": "Chrome",
"Version": "118.0.1"
}
}
}
```
</details>

## Prerequisites

Expand Down Expand Up @@ -69,17 +211,17 @@ this module. The context for the module is stored in the `SecretVault` as a secr

To store user data, the module developer can create a new context that defines a "namespace" for the user configuration. So let's say a developer has
implemented this for the `GitHub` module, a user would log in using their details. The module would call upon `Context` functionality to create a new
context under the `GitHub` context.
context under the `GitHub` namespace.

Imagine a user called `BobMarley` logs in to the `GitHub` module. The following would exist in the context:

- `Context:GitHub` containing module configuration, like default user, host, and client ID to use if not otherwise specified.
- `Context:GitHub.BobMarley` containing user configuration, details about the user, secrets and default values for API calls etc.
- `Context:GitHub/BobMarley` containing user configuration, details about the user, secrets and default values for API calls etc.

Let's say the person also has another account on `GitHub` called `RastaBlasta`. After logging on with the second account, the following context would
also exist in the context:

- `Context:GitHub.RastaBlasta` containing user configuration, details about the user, secrets and default values for API calls etc.
- `Context:GitHub/RastaBlasta` containing user configuration, details about the user, secrets and default values for API calls etc.

With this the module developer could allow users to set default context, and store a key of the name of that context in the module context. This way
the module could automatically log in the user to the correct account when the module is loaded. The user could also switch between accounts by
Expand All @@ -89,7 +231,7 @@ changing the default context.

To set up a new module to use the `Context` module, the following steps should be taken:

1. Create a new context for the module -> `Set-Context -Name 'GitHub'` during the module initialization.
1. Create a new context for the module -> `Set-Context -ID 'GitHub' -Context @{ ... }` during the module initialization.

`src\variable\private\Config.ps1`
```pwsh
Expand All @@ -100,10 +242,12 @@ $script:Config = @{

`src\loader.ps1`
```pwsh
Write-Verbose "Initialized secret vault [$($script:Config.VaultName)] of type [$($script:Config.VaultType)]"
### This is the context config for this module
$contextParams = @{
Name = $script:Config.Name
ID = 'GitHub'
Context = @{
Name = 'GitHub'
}
}
try {
Set-Context @contextParams
Expand All @@ -113,10 +257,10 @@ try {
}
```

2. Add some module configuration -> `Set-ContextSetting -Context 'GitHub' -Name 'ClientId' -Value '123456'`
3. Get the module configuration -> `Get-ContextSetting -Context 'GitHub' -Name 'ClientId'` -> `123456`
- `Get-ContextSettign -Context 'GitHub'` -> Returns all module configuration for the `GitHub` context.
4. Remove the module configuration -> `Remove-ContextSetting -Context 'GitHub' -Name 'ClientId'`
2. Add some module configuration -> `Set-ContextSetting -ID 'GitHub' -Name 'ClientId' -Value '123456'`
3. Get the module configuration -> `Get-ContextSetting -ID 'GitHub' -Name 'ClientId'` -> `123456`
- `Get-ContextSettign -ID 'GitHub'` -> Returns all module configuration for the `GitHub` context.
4. Remove the module configuration -> `Remove-ContextSetting -ID 'GitHub' -Name 'ClientId'`

### Setup for a New Context

Expand All @@ -132,11 +276,12 @@ To set up a new context for a user, the following steps should be taken:
- `Get-<ModuleName>ContextSetting` that uses `Get-ContextSetting`
- `Remove-<ModuleName>ContextSetting` that uses `Remove-ContextSetting`

2. Create a new context for the user -> `Set-Context -Context 'GitHub.BobMarley'` -> Context `GitHub.BobMarley` is created.
3. Add some user configuration -> `Set-ContextSetting -Context 'GitHub.BobMarley.AccessToken' -Name 'Secret' -Value '123456'` ->
Secret `GitHub.BobMarley.AccessToken` is created.
4. Get the user configuration -> `Get-ContextSetting -Context 'GitHub.BobMarley.AccessToken' -Name 'Secret' -AsPlainText` -> `123456`
5. Remove the user configuration -> `Remove-Context -Name 'GitHub.BobMarley.AccessToken'` -> Secret `GitHub.BobMarley.AccessToken` is removed.
2. Create a new context for the user -> `Set-Context -ID 'GitHub.BobMarley'` -> Context `GitHub/BobMarley` is created.
3. Add some user configuration -> `Set-ContextSetting -ID 'GitHub.BobMarley' -Name 'AccessToken' -Value 'qweqweqwe'` ->
Secret `GitHub.BobMarley` is created with a JSON structure containing the `AccessToken` secret.
4. Get the user configuration -> `Get-ContextSetting -Context 'GitHub/BobMarley' -Name 'AccessToken'` -> `qweqweqwe`
5. Remove the user configuration -> `Remove-Context -ID 'GitHub/BobMarley' -Name 'AccessToken` -> Secret `GitHub/BobMarley` is opened, the property
called `AccessToken` is removed, the context gets stored again.

## Contributing

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@
Write-Debug "Converting [$key] as [SecureString]"
$secureValue = $value -replace '^\[SECURESTRING\]', ''
$result | Add-Member -NotePropertyName $key -NotePropertyValue ($secureValue | ConvertTo-SecureString -AsPlainText -Force)
} elseif ($value -is [System.Collections.IEnumerable] -and ($value -isnot [string])) {
} elseif ($value -is [hashtable]) {
Write-Debug "Converting [$key] as [hashtable]"
$result | Add-Member -NotePropertyName $key -NotePropertyValue (Convert-ContextHashtableToObjectRecursive $value)
} elseif ($value -is [array]) {
Write-Debug "Converting [$key] as [IEnumerable], including arrays and hashtables"
$result | Add-Member -NotePropertyName $key -NotePropertyValue @(
$value | ForEach-Object {
Expand All @@ -52,9 +55,6 @@
}
}
)
} elseif ($value -is [hashtable]) {
Write-Debug "Converting [$key] as [hashtable]"
$result | Add-Member -NotePropertyName $key -NotePropertyValue (Convert-ContextHashtableToObjectRecursive $value)
} else {
Write-Debug "Converting [$key] as regular value"
$result | Add-Member -NotePropertyName $key -NotePropertyValue $value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,16 @@
Write-Debug '- as SecureString'
$value = $value | ConvertFrom-SecureString -AsPlainText
$result[$property.Name] = "[SECURESTRING]$value"
} elseif ($value -is [psobject] -or $value -is [PSCustomObject] -or $value -is [hashtable]) {
Write-Debug '- as PSObject, PSCustomObject or hashtable'
$result[$property.Name] = Convert-ContextObjectToHashtableRecursive $value
} elseif ($value -is [System.Collections.IEnumerable]) {
Write-Debug '- as IEnumerable, including arrays and hashtables'
$result[$property.Name] = @(
$value | ForEach-Object {
Convert-ContextObjectToHashtableRecursive $_
}
)
} elseif ($value -is [psobject] -or $value -is [PSCustomObject]) {
Write-Debug '- as PSObject, PSCustomObject'
$result[$property.Name] = Convert-ContextObjectToHashtableRecursive $value
} else {
Write-Debug '- as regular value'
$result[$property.Name] = $value
Expand Down
4 changes: 4 additions & 0 deletions tests/Context.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -265,17 +265,21 @@ Describe 'Context' {
}
}
Set-Context -Context $githubLoginContext -ID 'BigComplexObject'
Write-Verbose (Get-Secret -Name 'Context:BigComplexObject' -AsPlainText) -Verbose
$object = Get-Context -ID 'BigComplexObject'
$object.ApiRateLimits.Remaining | Should -Be 4985
$object.AuthToken | Should -BeOfType [System.Security.SecureString]
$object.AuthToken | ConvertFrom-SecureString -AsPlainText | Should -Be 'ghp_12345ABCDE67890FGHIJ'
$object.LastLoginAttempts[0].IP | Should -BeOfType [System.Security.SecureString]
$object.LastLoginAttempts[0].IP | ConvertFrom-SecureString -AsPlainText | Should -Be '192.168.1.101'
$object.LoginTime | Should -BeOfType [datetime]
$object.Repositories[0].Languages | Should -Be @('Python', 'JavaScript')
$object.Repositories[1].IsPrivate | Should -BeOfType [bool]
$object.Repositories[1].IsPrivate | Should -Be $false
$object.SessionMetaData.Location.City | Should -BeOfType [string]
$object.SessionMetaData.Location.City | Should -Be 'New York'
$object.UserPreferences | Should -BeOfType [PSCustomObject]
$object.UserPreferences.GetType().Name | Should -Be 'PSCustomObject'
$object.UserPreferences.CodeReview.GetType().BaseType.Name | Should -Be 'Array'
$object.UserPreferences.CodeReview.Count | Should -Be 2
$object.UserPreferences.CodeReview | Should -Be @('PR Comments', 'Inline Suggestions')
Expand Down
Loading