Skip to content

Commit 889ef4d

Browse files
🪲 [Fix]: Update docs (#50)
## Description - Update docs to be current with 3.0 - Fixes #49 - Add tests for DateTime and UserPreferences (hashtable -> pscustomobject) types. - Fix issue where a single nested hashtable becomes an array of a single object. - Fixes #51 ## Type of change <!-- Use the check-boxes [x] on the options that are relevant. --> - [x] 📖 [Docs] - [x] 🪲 [Fix] - [ ] 🩹 [Patch] - [ ] ⚠️ [Security fix] - [ ] 🚀 [Feature] - [ ] 🌟 [Breaking change] ## Checklist <!-- Use the check-boxes [x] on the options that are relevant. --> - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas
1 parent f6819cd commit 889ef4d

File tree

4 files changed

+191
-42
lines changed

4 files changed

+191
-42
lines changed

README.md

Lines changed: 180 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,35 +5,177 @@ we aim to store this data using a the concept of `Contexts` that are stored loca
55
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
66
without having to reconfigure the module or log in to services that support refreshing sessions with data you can store, i.e., refresh tokens.
77

8+
## What is a `Context`?
9+
810
The consept of `Contexts` is built on top of the functionality provided by the `Microsoft.PowerShell.SecretManagement` and
911
`Microsoft.PowerShell.SecretStore` modules. The `Context` module manages a set of `secrets` that is stored in a `SecretVault` instance. A context in
10-
this case is a collection of secrets and data that is combined to represent a context for a module or a user.
11-
12-
## What is a `Context`?
12+
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
13+
in the `SecretStore`. The `Context` is stored in the `SecretVault` as a secret with the name `Context:<ContextId>`.
1314

14-
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:
15+
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
16+
that is stored in the `SecretStore` and the other shows the same data as a `PSCustomObject` that could be used in a PowerShell script.
1517

16-
- Byte[]
17-
- String
18-
- SecureString
19-
- PSCredential
20-
- Hashtable
18+
<details>
19+
<summary>PSCustomObject</summary>
2120

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

2423
```pwsh
25-
@{
26-
Name = "GitHub" # Required: Used to store the context in the vault.
27-
AccessToken = "123456",
28-
AccessTokenExpirationDate = '2021-12-31T23:59:59'
29-
RefreshToken = '654321'
30-
RefreshTokenExpirationDate = '2021-12-31T23:59:59'
31-
APIVersion = 'v3'
32-
APIHost = 'https://api.github.com'
33-
ClientId = '123456'
34-
Scope = 'repo, user'
24+
[PSCustomObject]@{
25+
Username = 'john_doe'
26+
AuthToken = 'ghp_12345ABCDE67890FGHIJ' | ConvertTo-SecureString -AsPlainText -Force #gitleaks:allow
27+
LoginTime = Get-Date
28+
IsTwoFactorAuth = $true
29+
TwoFactorMethods = @('TOTP', 'SMS')
30+
LastLoginAttempts = @(
31+
[PSCustomObject]@{
32+
Timestamp = (Get-Date).AddHours(-1)
33+
IP = '192.168.1.101' | ConvertTo-SecureString -AsPlainText -Force
34+
Success = $true
35+
},
36+
[PSCustomObject]@{
37+
Timestamp = (Get-Date).AddDays(-1)
38+
IP = '203.0.113.5' | ConvertTo-SecureString -AsPlainText -Force
39+
Success = $false
40+
}
41+
)
42+
UserPreferences = @{
43+
Theme = 'dark'
44+
DefaultBranch = 'main'
45+
Notifications = [PSCustomObject]@{
46+
Email = $true
47+
Push = $false
48+
SMS = $true
49+
}
50+
CodeReview = @('PR Comments', 'Inline Suggestions')
51+
}
52+
Repositories = @(
53+
[PSCustomObject]@{
54+
Name = 'Repo1'
55+
IsPrivate = $true
56+
CreatedDate = (Get-Date).AddMonths(-6)
57+
Stars = 42
58+
Languages = @('Python', 'JavaScript')
59+
},
60+
[PSCustomObject]@{
61+
Name = 'Repo2'
62+
IsPrivate = $false
63+
CreatedDate = (Get-Date).AddYears(-1)
64+
Stars = 130
65+
Languages = @('C#', 'HTML', 'CSS')
66+
}
67+
)
68+
AccessScopes = @('repo', 'user', 'gist', 'admin:org')
69+
ApiRateLimits = [PSCustomObject]@{
70+
Limit = 5000
71+
Remaining = 4985
72+
ResetTime = (Get-Date).AddMinutes(30)
73+
}
74+
SessionMetaData = [PSCustomObject]@{
75+
SessionID = 'sess_abc123'
76+
Device = 'Windows-PC'
77+
Location = [PSCustomObject]@{
78+
Country = 'USA'
79+
City = 'New York'
80+
}
81+
BrowserInfo = [PSCustomObject]@{
82+
Name = 'Chrome'
83+
Version = '118.0.1'
84+
}
85+
}
86+
}
87+
```
88+
</details>
89+
90+
<details>
91+
<summary>JSON</summary>
92+
93+
This is same as what is stored, except that this is an uncomressed version for readability.
94+
95+
```json
96+
{
97+
"Username": "john_doe",
98+
"AuthToken": "[SECURESTRING]ghp_12345ABCDE67890FGHIJ",
99+
"LoginTime": "2024-11-21T21:16:56.2518249+01:00",
100+
"IsTwoFactorAuth": true,
101+
"TwoFactorMethods": [
102+
"TOTP",
103+
"SMS"
104+
],
105+
"LastLoginAttempts": [
106+
{
107+
"Timestamp": "2024-11-21T20:16:56.2518510+01:00",
108+
"IP": "[SECURESTRING]192.168.1.101",
109+
"Success": true
110+
},
111+
{
112+
"Timestamp": "2024-11-20T21:16:56.2529436+01:00",
113+
"IP": "[SECURESTRING]203.0.113.5",
114+
"Success": false
115+
}
116+
],
117+
"UserPreferences": {
118+
"Theme": "dark",
119+
"DefaultBranch": "main",
120+
"Notifications": {
121+
"Email": true,
122+
"Push": false,
123+
"SMS": true
124+
},
125+
"CodeReview": [
126+
"PR Comments",
127+
"Inline Suggestions"
128+
]
129+
},
130+
"Repositories": [
131+
{
132+
"Name": "Repo1",
133+
"IsPrivate": true,
134+
"CreatedDate": "2024-05-21T21:16:56.2540703+02:00",
135+
"Stars": 42,
136+
"Languages": [
137+
"Python",
138+
"JavaScript"
139+
]
140+
},
141+
{
142+
"Name": "Repo2",
143+
"IsPrivate": false,
144+
"CreatedDate": "2023-11-21T21:16:56.2545789+01:00",
145+
"Stars": 130,
146+
"Languages": [
147+
"C#",
148+
"HTML",
149+
"CSS"
150+
]
151+
}
152+
],
153+
"AccessScopes": [
154+
"repo",
155+
"user",
156+
"gist",
157+
"admin:org"
158+
],
159+
"ApiRateLimits": {
160+
"Limit": 5000,
161+
"Remaining": 4985,
162+
"ResetTime": "2024-11-21T21:46:56.2550348+01:00"
163+
},
164+
"SessionMetaData": {
165+
"SessionID": "sess_abc123",
166+
"Device": "Windows-PC",
167+
"Location": {
168+
"Country": "USA",
169+
"City": "New York"
170+
},
171+
"BrowserInfo": {
172+
"Name": "Chrome",
173+
"Version": "118.0.1"
174+
}
175+
}
35176
}
36177
```
178+
</details>
37179

38180
## Prerequisites
39181

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

70212
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
71213
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
72-
context under the `GitHub` context.
214+
context under the `GitHub` namespace.
73215

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

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

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

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

84226
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
85227
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
@@ -89,7 +231,7 @@ changing the default context.
89231

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

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

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

101243
`src\loader.ps1`
102244
```pwsh
103-
Write-Verbose "Initialized secret vault [$($script:Config.VaultName)] of type [$($script:Config.VaultType)]"
104245
### This is the context config for this module
105246
$contextParams = @{
106-
Name = $script:Config.Name
247+
ID = 'GitHub'
248+
Context = @{
249+
Name = 'GitHub'
250+
}
107251
}
108252
try {
109253
Set-Context @contextParams
@@ -113,10 +257,10 @@ try {
113257
}
114258
```
115259

116-
2. Add some module configuration -> `Set-ContextSetting -Context 'GitHub' -Name 'ClientId' -Value '123456'`
117-
3. Get the module configuration -> `Get-ContextSetting -Context 'GitHub' -Name 'ClientId'` -> `123456`
118-
- `Get-ContextSettign -Context 'GitHub'` -> Returns all module configuration for the `GitHub` context.
119-
4. Remove the module configuration -> `Remove-ContextSetting -Context 'GitHub' -Name 'ClientId'`
260+
2. Add some module configuration -> `Set-ContextSetting -ID 'GitHub' -Name 'ClientId' -Value '123456'`
261+
3. Get the module configuration -> `Get-ContextSetting -ID 'GitHub' -Name 'ClientId'` -> `123456`
262+
- `Get-ContextSettign -ID 'GitHub'` -> Returns all module configuration for the `GitHub` context.
263+
4. Remove the module configuration -> `Remove-ContextSetting -ID 'GitHub' -Name 'ClientId'`
120264

121265
### Setup for a New Context
122266

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

135-
2. Create a new context for the user -> `Set-Context -Context 'GitHub.BobMarley'` -> Context `GitHub.BobMarley` is created.
136-
3. Add some user configuration -> `Set-ContextSetting -Context 'GitHub.BobMarley.AccessToken' -Name 'Secret' -Value '123456'` ->
137-
Secret `GitHub.BobMarley.AccessToken` is created.
138-
4. Get the user configuration -> `Get-ContextSetting -Context 'GitHub.BobMarley.AccessToken' -Name 'Secret' -AsPlainText` -> `123456`
139-
5. Remove the user configuration -> `Remove-Context -Name 'GitHub.BobMarley.AccessToken'` -> Secret `GitHub.BobMarley.AccessToken` is removed.
279+
2. Create a new context for the user -> `Set-Context -ID 'GitHub.BobMarley'` -> Context `GitHub/BobMarley` is created.
280+
3. Add some user configuration -> `Set-ContextSetting -ID 'GitHub.BobMarley' -Name 'AccessToken' -Value 'qweqweqwe'` ->
281+
Secret `GitHub.BobMarley` is created with a JSON structure containing the `AccessToken` secret.
282+
4. Get the user configuration -> `Get-ContextSetting -Context 'GitHub/BobMarley' -Name 'AccessToken'` -> `qweqweqwe`
283+
5. Remove the user configuration -> `Remove-Context -ID 'GitHub/BobMarley' -Name 'AccessToken` -> Secret `GitHub/BobMarley` is opened, the property
284+
called `AccessToken` is removed, the context gets stored again.
140285

141286
## Contributing
142287

src/functions/private/JsonToObject/Convert-ContextHashtableToObjectRecursive.ps1

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@
4141
Write-Debug "Converting [$key] as [SecureString]"
4242
$secureValue = $value -replace '^\[SECURESTRING\]', ''
4343
$result | Add-Member -NotePropertyName $key -NotePropertyValue ($secureValue | ConvertTo-SecureString -AsPlainText -Force)
44-
} elseif ($value -is [System.Collections.IEnumerable] -and ($value -isnot [string])) {
44+
} elseif ($value -is [hashtable]) {
45+
Write-Debug "Converting [$key] as [hashtable]"
46+
$result | Add-Member -NotePropertyName $key -NotePropertyValue (Convert-ContextHashtableToObjectRecursive $value)
47+
} elseif ($value -is [array]) {
4548
Write-Debug "Converting [$key] as [IEnumerable], including arrays and hashtables"
4649
$result | Add-Member -NotePropertyName $key -NotePropertyValue @(
4750
$value | ForEach-Object {
@@ -52,9 +55,6 @@
5255
}
5356
}
5457
)
55-
} elseif ($value -is [hashtable]) {
56-
Write-Debug "Converting [$key] as [hashtable]"
57-
$result | Add-Member -NotePropertyName $key -NotePropertyValue (Convert-ContextHashtableToObjectRecursive $value)
5858
} else {
5959
Write-Debug "Converting [$key] as regular value"
6060
$result | Add-Member -NotePropertyName $key -NotePropertyValue $value

src/functions/private/ObjectToJSON/Convert-ContextObjectToHashtableRecursive.ps1

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,16 @@
5151
Write-Debug '- as SecureString'
5252
$value = $value | ConvertFrom-SecureString -AsPlainText
5353
$result[$property.Name] = "[SECURESTRING]$value"
54+
} elseif ($value -is [psobject] -or $value -is [PSCustomObject] -or $value -is [hashtable]) {
55+
Write-Debug '- as PSObject, PSCustomObject or hashtable'
56+
$result[$property.Name] = Convert-ContextObjectToHashtableRecursive $value
5457
} elseif ($value -is [System.Collections.IEnumerable]) {
5558
Write-Debug '- as IEnumerable, including arrays and hashtables'
5659
$result[$property.Name] = @(
5760
$value | ForEach-Object {
5861
Convert-ContextObjectToHashtableRecursive $_
5962
}
6063
)
61-
} elseif ($value -is [psobject] -or $value -is [PSCustomObject]) {
62-
Write-Debug '- as PSObject, PSCustomObject'
63-
$result[$property.Name] = Convert-ContextObjectToHashtableRecursive $value
6464
} else {
6565
Write-Debug '- as regular value'
6666
$result[$property.Name] = $value

tests/Context.Tests.ps1

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,17 +265,21 @@ Describe 'Context' {
265265
}
266266
}
267267
Set-Context -Context $githubLoginContext -ID 'BigComplexObject'
268+
Write-Verbose (Get-Secret -Name 'Context:BigComplexObject' -AsPlainText) -Verbose
268269
$object = Get-Context -ID 'BigComplexObject'
269270
$object.ApiRateLimits.Remaining | Should -Be 4985
270271
$object.AuthToken | Should -BeOfType [System.Security.SecureString]
271272
$object.AuthToken | ConvertFrom-SecureString -AsPlainText | Should -Be 'ghp_12345ABCDE67890FGHIJ'
272273
$object.LastLoginAttempts[0].IP | Should -BeOfType [System.Security.SecureString]
273274
$object.LastLoginAttempts[0].IP | ConvertFrom-SecureString -AsPlainText | Should -Be '192.168.1.101'
275+
$object.LoginTime | Should -BeOfType [datetime]
274276
$object.Repositories[0].Languages | Should -Be @('Python', 'JavaScript')
275277
$object.Repositories[1].IsPrivate | Should -BeOfType [bool]
276278
$object.Repositories[1].IsPrivate | Should -Be $false
277279
$object.SessionMetaData.Location.City | Should -BeOfType [string]
278280
$object.SessionMetaData.Location.City | Should -Be 'New York'
281+
$object.UserPreferences | Should -BeOfType [PSCustomObject]
282+
$object.UserPreferences.GetType().Name | Should -Be 'PSCustomObject'
279283
$object.UserPreferences.CodeReview.GetType().BaseType.Name | Should -Be 'Array'
280284
$object.UserPreferences.CodeReview.Count | Should -Be 2
281285
$object.UserPreferences.CodeReview | Should -Be @('PR Comments', 'Inline Suggestions')

0 commit comments

Comments
 (0)