Pre-populate partition on sso login

This commit is contained in:
wenincode 2022-10-19 17:26:25 -06:00
parent 702b63d819
commit f89fc309ff
7 changed files with 297 additions and 285 deletions

View File

@ -51,8 +51,10 @@
/>
{{#if (can "use SSO")}}
<authForm.Method @matches="sso">
{{log (concat "Partition Parent: " @partition)}}
<OidcSelect
@dc={{@dc.Name}}
@partition={{@partition}}
@nspace={{@nspace}}
@disabled={{authForm.disabled}}
@onchange={{authForm.submit}}

View File

@ -1,164 +1,142 @@
<StateChart
@src={{this.chart}}
as |State Guard ChartAction dispatch state|>
{{#let
(hash
State=State
Guard=Guard
Action=ChartAction
dispatch=dispatch
state=state
)
as |chart|}}
{{#let
(hash
reset=(action dispatch "RESET")
focus=this.focus
disabled=(state-matches state "loading")
error=(queue
(action dispatch "ERROR")
(action (mut this.error) value="error.errors.firstObject")
)
submit=(queue
(action (mut this.value))
(action dispatch "SUBMIT")
)
)
as |exported|}}
<Guard
@name="hasValue"
@cond={{this.hasValue}}
/>
{{!TODO: Call this reset or similar }}
<chart.Action
@name="clearError"
@exec={{queue (action (mut this.error) undefined) (action (mut this.secret) undefined)}}
/>
<div
class="auth-form"
...attributes
>
<StateChart
@src={{this.tabsChart}}
as |TabState IgnoredGuard IgnoredAction tabDispatch tabState|>
{{#if (can 'use SSO')}}
<TabNav
@items={{array
(hash
label='Token'
selected=(state-matches tabState 'token')
<StateChart @src={{this.chart}} as |State Guard ChartAction dispatch state|>
{{#let
(hash State=State Guard=Guard Action=ChartAction dispatch=dispatch state=state)
as |chart|
}}
{{#let
(hash
reset=(action dispatch 'RESET')
focus=this.focus
disabled=(state-matches state 'loading')
error=(queue
(action dispatch 'ERROR') (action (mut this.error) value='error.errors.firstObject')
)
(hash
label='SSO'
selected=(state-matches tabState 'sso')
)
}}
@onclick={{queue (action tabDispatch) (action dispatch "RESET")}}
/>
{{/if}}
<State @matches="error">
{{#if this.error.status}}
<Notice
@type="error"
role="alert"
as |notice|>
<notice.Body>
<p>
{{#if this.value.Name}}
{{#if (eq this.error.status '403')}}
<strong>Consul login failed</strong><br />
We received a token from your OIDC provider but could not log in to Consul with it.
{{else if (eq this.error.status '401')}}
<strong>Could not log in to provider</strong><br />
The OIDC provider has rejected this access token. Please have an administrator check your auth method configuration.
{{else if (eq this.error.status '499')}}
<strong>SSO log in window closed</strong><br />
The OIDC provider window was closed. Please try again.
{{else}}
<strong>Error</strong><br />
{{this.error.detail}}
{{/if}}
{{else}}
{{#if (eq this.error.status '403')}}
<strong>Invalid token</strong><br />
The token entered does not exist. Please enter a valid token to log in.
{{else if (eq this.error.status '404')}}
<strong>No providers</strong><br />
No SSO providers are configured for that Partition.
{{else}}
<strong>Error</strong><br />
{{this.error.detail}}
{{/if}}
{{/if}}
</p>
</notice.Body>
</Notice>
{{/if}}
</State>
<TabState @matches="token">
<form
onsubmit={{action dispatch "SUBMIT"}}
>
<fieldset>
<label
class={{concat "type-password" (if (and (state-matches state 'error') (not this.error.status)) ' has-error')}}
submit=(queue (action (mut this.value)) (action dispatch 'SUBMIT'))
)
as |exported|
}}
<Guard @name='hasValue' @cond={{this.hasValue}} />
{{!TODO: Call this reset or similar }}
<chart.Action
@name='clearError'
@exec={{queue (action (mut this.error) undefined) (action (mut this.secret) undefined)}}
/>
<div class='auth-form' ...attributes>
<StateChart
@src={{this.tabsChart}}
as |TabState IgnoredGuard IgnoredAction tabDispatch tabState|
>
<span>Log in with a token</span>
{{! Blink/Webkit based seem to leak password inputs }}
{{! this will only occur during acceptance testing so }}
{{! turn them into text inputs during acceptance testing }}
<input
{{did-insert (set this 'input')}}
disabled={{state-matches state "loading"}}
type={{if (eq (env 'environment') 'testing') 'text' 'password'}}
name="auth[SecretID]"
placeholder="SecretID"
value={{this.secret}}
oninput={{queue
(action (mut this.secret) value="target.value")
(action (mut this.value) value="target.value")
(action dispatch "TYPING")
}}
/>
<State @matches="error">
{{#if (not this.error.status)}}
<strong role="alert">
Please enter your secret
</strong>
{{#if (can 'use SSO')}}
<TabNav
@items={{array
(hash label='Token' selected=(state-matches tabState 'token'))
(hash label='SSO' selected=(state-matches tabState 'sso'))
}}
@onclick={{queue (action tabDispatch) (action dispatch 'RESET')}}
/>
{{/if}}
<State @matches='error'>
{{#if this.error.status}}
<Notice @type='error' role='alert' as |notice|>
<notice.Body>
<p>
{{#if this.value.Name}}
{{#if (eq this.error.status '403')}}
<strong>Consul login failed</strong><br />
We received a token from your OIDC provider but could not log in to Consul
with it.
{{else if (eq this.error.status '401')}}
<strong>Could not log in to provider</strong><br />
The OIDC provider has rejected this access token. Please have an
administrator check your auth method configuration.
{{else if (eq this.error.status '499')}}
<strong>SSO log in window closed</strong><br />
The OIDC provider window was closed. Please try again.
{{else}}
<strong>Error</strong><br />
{{this.error.detail}}
{{/if}}
{{else}}
{{#if (eq this.error.status '403')}}
<strong>Invalid token</strong><br />
The token entered does not exist. Please enter a valid token to log in.
{{else if (eq this.error.status '404')}}
<strong>No providers</strong><br />
No SSO providers are configured for that Partition.
{{else}}
<strong>Error</strong><br />
{{this.error.detail}}
{{/if}}
{{/if}}
</p>
</notice.Body>
</Notice>
{{/if}}
</State>
</label>
</fieldset>
<Action
@type="submit"
disabled={{state-matches state "loading"}}
>
Log in
</Action>
</form>
</TabState>
<TabState @matches='token'>
<form onsubmit={{action dispatch 'SUBMIT'}}>
<fieldset>
<label
class={{concat
'type-password'
(if (and (state-matches state 'error') (not this.error.status)) ' has-error')
}}
>
<span>Log in with a token</span>
{{yield (assign exported (hash Method=TabState))}}
{{! Blink/Webkit based seem to leak password inputs }}
{{! this will only occur during acceptance testing so }}
{{! turn them into text inputs during acceptance testing }}
<input
{{did-insert (set this 'input')}}
disabled={{state-matches state 'loading'}}
type={{if (eq (env 'environment') 'testing') 'text' 'password'}}
name='auth[SecretID]'
placeholder='SecretID'
value={{this.secret}}
oninput={{queue
(action (mut this.secret) value='target.value')
(action (mut this.value) value='target.value')
(action dispatch 'TYPING')
}}
/>
<State @matches='error'>
{{#if (not this.error.status)}}
<strong role='alert'>
Please enter your secret
</strong>
{{/if}}
</State>
</label>
</fieldset>
<Action @type='submit' disabled={{state-matches state 'loading'}}>
Log in
</Action>
</form>
</TabState>
<em>
Contact your administrator for login credentials.
</em>
</StateChart>
</div>
<State @matches="loading">
<TokenSource
@dc={{@dc}}
@nspace={{or this.value.Namespace @nspace}}
@partition={{or this.value.Partition @partition}}
@type={{if this.value.Name 'oidc' 'secret'}}
@value={{this.value}}
@onchange={{queue (action dispatch "RESET") @onsubmit}}
@onerror={{queue (action (mut this.error) value="error.errors.firstObject") (action dispatch "ERROR")}}
/>
</State>
{{/let}}
{{/let}}
{{yield (assign exported (hash Method=TabState))}}
<em>
Contact your administrator for login credentials.
</em>
</StateChart>
</div>
<State @matches='loading'>
<TokenSource
@dc={{@dc}}
@nspace={{or this.value.Namespace @nspace}}
@partition={{or this.value.Partition @partition}}
@type={{if this.value.Name 'oidc' 'secret'}}
@value={{this.value}}
@onchange={{queue (action dispatch 'RESET') @onsubmit}}
@onerror={{queue
(action (mut this.error) value='error.errors.firstObject')
(action dispatch 'ERROR')
}}
/>
</State>
{{/let}}
{{/let}}
</StateChart>

View File

@ -1,134 +1,117 @@
<StateChart
@src={{chart}}
as |State Guard ChartAction dispatch state|>
{{#let
(hash
State=State
Guard=Guard
Action=ChartAction
dispatch=dispatch
state=state
)
as |chart|}}
<StateChart @src={{chart}} as |State Guard ChartAction dispatch state|>
{{#let
(hash State=State Guard=Guard Action=ChartAction dispatch=dispatch state=state)
as |chart|
}}
<div
class="oidc-select"
...attributes
>
<State @notMatches="idle">
<DataSource
@src={{uri '/${partition}/${nspace}/${dc}/oidc/providers'
(hash
partition=this.partition
nspace=@nspace
dc=@dc
)
}}
@onchange={{queue (action (mut this.items) value="data") (fn dispatch "SUCCESS")}}
@onerror={{queue (fn dispatch "RESET") @onerror}}
/>
</State>
<State @matches="loaded">
<Action
{{on 'click' (queue (set this 'partition' '') (fn dispatch "RESET"))}}
class="reset"
>
Choose different Partition
</Action>
</State>
<StateChart
@src={{state-chart 'validate'}}
as |ignoredState ignoredGuard ignoredAction formDispatch state|>
<TextInput
@name="partition"
@label="Admin Partition"
@item={{this}}
@validations={{hash
partition=(array
(hash
test='^[a-zA-Z0-9]([a-zA-Z0-9-]{0,62}[a-zA-Z0-9])?$'
error='Name must be a valid DNS hostname.'
)
)
}}
@placeholder="Enter your Partition"
@oninput={{action (mut this.partition) value="target.value"}}
@chart={{hash
state=state
dispatch=formDispatch
}}
/>
{{! this belongs to the outer StateChart but we need }}
{{! to understand validation state }}
<State @matches="idle">
<Action
{{disabled (or (lt this.partition.length 1) (state-matches state "error"))}}
{{on "click" (fn dispatch "LOAD")}}
>
Choose provider
</Action>
</State>
</StateChart>
<State @matches="loading">
<Progress aria-label="Loading" />
</State>
<State @matches="loaded">
{{#if (lt this.items.length 3)}}
<ul>
{{#each this.items as |item|}}
<li>
<Action
class={{concat item.Kind '-oidc-provider'}}
disabled={{@disabled}}
@type="button"
{{on 'click' (fn @onchange item)}}
>
Continue with {{or item.DisplayName item.Name}}{{#if (not-eq item.Namespace 'default')}} ({{item.Namespace}}){{/if}}
</Action>
</li>
{{/each}}
</ul>
{{else}}
{{#let (or this.provider (object-at 0 this.items)) as |item|}}
<OptionInput
@label="SSO Provider"
@name="provider"
@item={{this}}
@selected={{item}}
@items={{this.items}}
@onchange={{action (mut this.provider)}}
@disabled={{@disabled}}
>
<:option as |option|>
<span
class={{concat option.item.Kind '-oidc-provider'}}
>
{{or option.item.DisplayName option.item.Name}}{{#if (not-eq option.item.Namespace 'default')}} ({{option.item.Namespace}}){{/if}}
</span>
</:option>
</OptionInput>
<div class='oidc-select' ...attributes>
<State @notMatches='idle'>
<DataSource
@src={{uri
'/${partition}/${nspace}/${dc}/oidc/providers'
(hash partition=this.partition nspace=@nspace dc=@dc)
}}
@onchange={{queue (action (mut this.items) value='data') (fn dispatch 'SUCCESS')}}
@onerror={{queue (fn dispatch 'RESET') @onerror}}
/>
</State>
<State @matches='loaded'>
<Action
@type="button"
{{disabled @disabled}}
{{on 'click' (fn @onchange item)}}
{{on 'click' (queue (set this 'partition' '') (fn dispatch 'RESET'))}}
class='reset'
>
Log in
Choose different Partition
</Action>
</State>
{{/let}}
{{/if}}
</State>
<StateChart
@src={{state-chart 'validate'}}
as |ignoredState ignoredGuard ignoredAction formDispatch state|
>
<TextInput
@name='partition'
@label='Admin Partition'
@item={{this}}
@validations={{hash
partition=(array
(hash
test='^[a-zA-Z0-9]([a-zA-Z0-9-]{0,62}[a-zA-Z0-9])?$'
error='Name must be a valid DNS hostname.'
)
)
}}
@placeholder='Enter your Partition'
@oninput={{action (mut this.partition) value='target.value'}}
@chart={{hash state=state dispatch=formDispatch}}
/>
{{! this belongs to the outer StateChart but we need }}
{{! to understand validation state }}
<State @matches='idle'>
<Action
{{disabled (or (lt this.partition.length 1) (state-matches state 'error'))}}
{{on 'click' (fn dispatch 'LOAD')}}
>
Choose provider
</Action>
</State>
</StateChart>
<State @matches='loading'>
<Progress aria-label='Loading' />
</State>
<State @matches='loaded'>
{{#if (lt this.items.length 3)}}
<ul>
{{#each this.items as |item|}}
<li>
<Action
class={{concat item.Kind '-oidc-provider'}}
disabled={{@disabled}}
@type='button'
{{on 'click' (fn @onchange item)}}
>
Continue with
{{or item.DisplayName item.Name}}{{#if (not-eq item.Namespace 'default')}}
({{item.Namespace}}){{/if}}
</Action>
</li>
{{/each}}
</ul>
{{else}}
{{#let (or this.provider (object-at 0 this.items)) as |item|}}
<OptionInput
@label='SSO Provider'
@name='provider'
@item={{this}}
@selected={{item}}
@items={{this.items}}
@onchange={{action (mut this.provider)}}
@disabled={{@disabled}}
>
<:option as |option|>
<span class={{concat option.item.Kind '-oidc-provider'}}>
{{or option.item.DisplayName option.item.Name}}{{#if
(not-eq option.item.Namespace 'default')
}} ({{option.item.Namespace}}){{/if}}
</span>
</:option>
</OptionInput>
<Action @type='button' {{disabled @disabled}} {{on 'click' (fn @onchange item)}}>
Log in
</Action>
{{/let}}
{{/if}}
</State>
</div>
{{/let}}
{{/let}}
</StateChart>

View File

@ -4,9 +4,14 @@ import { tracked } from '@glimmer/tracking';
import chart from './chart.xstate';
export default class OidcSelect extends Component {
@tracked partition = '';
@tracked partition = 'default';
constructor() {
super(...arguments);
this.chart = chart;
if (this.args.partition) {
this.partition = this.args.partition;
}
}
}

View File

@ -41,6 +41,7 @@ Feature: login
---
And I click login on the navigation
And I click "[data-test-tab=tab_sso] button"
Then the "[name='partition']" input should have the value "default"
And I type "partition" into "[name=partition]"
And I click ".oidc-select button"
Then a GET request was made to "/v1/internal/ui/oidc-auth-methods?dc=dc-1&ns=@namespace&partition=partition"
@ -49,3 +50,36 @@ Feature: login
And a POST request was made to "/v1/acl/oidc/callback?dc=dc-1&ns=@!namespace&partition=partition"
And "[data-notification]" has the "notification-authorize" class
And "[data-notification]" has the "success" class
Scenario: Logging in via SSO with a partition chosen
Given 1 datacenter model with the value "dc-1"
And SSO is enabled
And partitions are enabled
And 1 partition model with the value "_example-partition"
And 1 oidcProvider model from yaml
---
- DisplayName: Okta
Name: okta
Kind: okta
---
When I visit the services page for yaml
---
dc: dc-1
partition: example-partition
---
And the "okta" oidcProvider responds with from yaml
---
state: state-123456789/abcdefghijklmnopqrstuvwxyz
code: code-abcdefghijklmnopqrstuvwxyz/123456789
---
And I click login on the navigation
And I click "[data-test-tab=tab_sso] button"
Then the "[name='partition']" input should have the value "example-partition"
And I type "partition" into "[name=partition]"
And I click ".oidc-select button"
Then a GET request was made to "/v1/internal/ui/oidc-auth-methods?dc=dc-1&ns=@namespace&partition=partition"
And I click ".okta-oidc-provider"
Then a POST request was made to "/v1/acl/oidc/auth-url?dc=dc-1&ns=@!namespace&partition=partition"
And a POST request was made to "/v1/acl/oidc/callback?dc=dc-1&ns=@!namespace&partition=partition"
And "[data-notification]" has the "notification-authorize" class
And "[data-notification]" has the "success" class

View File

@ -66,6 +66,8 @@ export function visitable(path, encoder = encodeURIComponent) {
let path = paths.shift();
if (typeof dynamicSegmentsAndQueryParams.nspace !== 'undefined') {
path = `/:nspace${path}`;
} else if (typeof dynamicSegmentsAndQueryParams.partition !== 'undefined') {
path = `/:partition${path}`;
}
params = assign({}, dynamicSegmentsAndQueryParams);
let fullPath;

View File

@ -85,5 +85,13 @@ export default function (scenario, assert, pauseUntil, find, currentURL, clipboa
})
.then(['the title should be "$title"'], function (title) {
assert.equal(document.title, title, `Expected the document.title to equal "${title}"`);
})
.then(['the "$selector" input should have the value "$value"'], function (selector, value) {
const $el = find(selector);
assert.equal(
$el.value,
value,
`Expected the input at ${selector} to have value ${value}, but it had ${$el.value}`
);
});
}