-
Notifications
You must be signed in to change notification settings - Fork 4.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
UI: Improved Login/Logout flow inc SSO support (#7790)
* 6 new components for new login/logout flow, plus SSO support UI Components: 1. AuthDialog: Wraps/orchestrates AuthForm and AuthProfile 2. AuthForm: Authorization form shown when logged out. 3. AuthProfile: Simple presentational component to show the users 'Profile' 4. OidcSelect: A 'select' component for selecting an OIDC provider, dynamically uses either a single select menu or multiple buttons depending on the amount of providers Data Components: 1. JwtSource: Given an OIDC provider URL this component will request a token from the provider and fire an donchange event when it has been retrieved. Used by TokenSource. 2. TokenSource: Given a oidc provider name or a Consul SecretID, TokenSource will use whichever method/API requests required to retrieve Consul ACL Token, which is emitted to the onchange event handler. Very basic README documentation included here, which is likely to be refined somewhat. * CSS required for new auth/SSO UI components * Remaining app code required to tie the new auth/SSO work together * CSS code required to help tie the auth/SSO work together * Test code in order to get current tests passing with new auth/SSO flow ..plus extremely basics/skipped rendering tests for the new components * Treat the secret received from the server as the truth Previously we've always treated what the user typed as the truth, this breaks down when using SSO as the user doesn't type anything to retrieve a token. Therefore we change this so that we use the secret in the API response as the truth. * Make sure removing an dom tree from a buffer only removes its own tree
- Loading branch information
Showing
99 changed files
with
1,576 additions
and
344 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
## AuthDialog | ||
|
||
```handlebars | ||
<AuthDialog @dc={{dc}} @nspace={{}} @onchange={{action 'change'}} as |api components|> | ||
{{#let components.AuthForm components.AuthProfile as |AuthForm AuthProfile|}} | ||
<BlockSlot @name="unauthorized"> | ||
Here's the login form: | ||
<AuthForm /> | ||
</BlockSlot> | ||
<BlockSlot @name="authorized"> | ||
Here's your profile: | ||
<AuthProfile /> | ||
<button onclick={{action api.logout}} /> | ||
</BlockSlot> | ||
{{/let}} | ||
</AuthDialog> | ||
``` | ||
|
||
### Arguments | ||
|
||
A component to help orchestrate a login/logout flow. | ||
|
||
| Argument | Type | Default | Description | | ||
| --- | --- | --- | --- | | ||
| `dc` | `String` | | The name of the current datacenter | | ||
| `nspace` | `String` | | The name of the current namespace | | ||
| `onchange` | `Function` | | An action to fire when the users token has changed (logged in/logged out/token changed) | | ||
|
||
### Methods/Actions/api | ||
|
||
| Method/Action | Description | | ||
| --- | --- | | ||
| `login` | Login with a specified token | | ||
| `logout` | Logout (delete token) | | ||
| `token` | The current token itself (as a property not a method) | | ||
|
||
### Components | ||
|
||
| Name | Description | | ||
| --- | --- | | ||
| [`AuthForm`](../auth-form/README.mdx) | Renders an Authorization form | | ||
| [`AuthProfile`](../auth-profile/README.mdx) | Renders a User Profile | | ||
|
||
### Slots | ||
|
||
| Name | Description | | ||
| --- | --- | | ||
| `unauthorized` | This slot is only rendered when the user doesn't have a token | | ||
| `authorized` | This slot is only rendered whtn the user has a token.| | ||
|
||
### See | ||
|
||
- [Component Source Code](./index.js) | ||
- [Template Source Code](./index.hbs) | ||
|
||
--- |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
export default { | ||
id: 'auth-dialog', | ||
initial: 'idle', | ||
on: { | ||
CHANGE: [ | ||
{ | ||
target: 'authorized', | ||
cond: 'hasToken', | ||
actions: ['login'], | ||
}, | ||
{ | ||
target: 'unauthorized', | ||
actions: ['logout'], | ||
}, | ||
], | ||
}, | ||
states: { | ||
idle: { | ||
on: { | ||
CHANGE: [ | ||
{ | ||
target: 'authorized', | ||
cond: 'hasToken', | ||
}, | ||
{ | ||
target: 'unauthorized', | ||
}, | ||
], | ||
}, | ||
}, | ||
unauthorized: {}, | ||
authorized: {}, | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
<StateChart @src={{chart}} as |State Guard Action dispatch state|> | ||
|
||
<Guard @name="hasToken" @cond={{action 'hasToken'}} /> | ||
<Action @name="login" @exec={{action 'login'}} /> | ||
<Action @name="logout" @exec={{action 'logout'}} /> | ||
{{! This DataSource just permanently listens to any changes to the users }} | ||
{{! token, whether thats a new token, a changed token or a deleted token }} | ||
<DataSource | ||
@src="settings://consul:token" | ||
@onchange={{queue (action (mut token) value="data") (action dispatch "CHANGE") (action (mut previousToken) value="data")}} | ||
/> | ||
{{! This DataSink is just used for logging in from the form, }} | ||
{{! or logging out via the exposed logout function }} | ||
<DataSink | ||
@sink="settings://consul:token" | ||
as |sink| | ||
> | ||
{{yield}} | ||
{{#let (hash | ||
login=(action sink.open) | ||
logout=(action sink.open null) | ||
token=token | ||
) (hash | ||
AuthProfile=(component 'auth-profile' item=token) | ||
AuthForm=(component 'auth-form' dc=dc nspace=nspace onsubmit=(action sink.open value="data")) | ||
) as |api components|}} | ||
<State @matches="authorized"> | ||
{{#yield-slot name="authorized"}} | ||
{{yield api components}} | ||
{{/yield-slot}} | ||
</State> | ||
|
||
<State @matches="unauthorized"> | ||
{{#yield-slot name="unauthorized"}} | ||
{{yield api components}} | ||
{{/yield-slot}} | ||
</State> | ||
{{/let}} | ||
</DataSink> | ||
</StateChart> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import Component from '@ember/component'; | ||
import Slotted from 'block-slots'; | ||
import { inject as service } from '@ember/service'; | ||
import { get } from '@ember/object'; | ||
import chart from './chart.xstate'; | ||
|
||
export default Component.extend(Slotted, { | ||
tagName: '', | ||
repo: service('repository/oidc-provider'), | ||
init: function() { | ||
this._super(...arguments); | ||
this.chart = chart; | ||
}, | ||
actions: { | ||
hasToken: function() { | ||
return typeof this.token.AccessorID !== 'undefined'; | ||
}, | ||
login: function() { | ||
let prev = get(this, 'previousToken.AccessorID'); | ||
let current = get(this, 'token.AccessorID'); | ||
if (prev === null) { | ||
prev = get(this, 'previousToken.SecretID'); | ||
} | ||
if (current === null) { | ||
current = get(this, 'token.SecretID'); | ||
} | ||
let type = 'authorize'; | ||
if (typeof prev !== 'undefined' && prev !== current) { | ||
type = 'use'; | ||
} | ||
this.onchange({ data: get(this, 'token'), type: type }); | ||
}, | ||
logout: function() { | ||
if (typeof get(this, 'previousToken.AuthMethod') !== 'undefined') { | ||
// we are ok to fire and forget here | ||
this.repo.logout(get(this, 'previousToken.SecretID')); | ||
} | ||
this.previousToken = null; | ||
this.onchange({ data: null, type: 'logout' }); | ||
}, | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
## AuthForm | ||
|
||
```handlebars | ||
<AuthForm as |api|></AuthForm> | ||
``` | ||
|
||
### Methods/Actions/api | ||
|
||
| Method/Action | Description | | ||
| --- | --- | | ||
| `reset` | Reset the form back to its original empty/non-error state | | ||
|
||
### See | ||
|
||
- [Component Source Code](./index.js) | ||
- [Template Source Code](./index.hbs) | ||
|
||
--- |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
export default { | ||
id: 'auth-form', | ||
initial: 'idle', | ||
on: { | ||
RESET: [ | ||
{ | ||
target: 'idle', | ||
}, | ||
], | ||
}, | ||
states: { | ||
idle: { | ||
entry: ['clearError'], | ||
on: { | ||
SUBMIT: [ | ||
{ | ||
target: 'loading', | ||
cond: 'hasValue', | ||
}, | ||
{ | ||
target: 'error', | ||
}, | ||
], | ||
}, | ||
}, | ||
loading: { | ||
on: { | ||
ERROR: [ | ||
{ | ||
target: 'error', | ||
}, | ||
], | ||
}, | ||
}, | ||
error: { | ||
exit: ['clearError'], | ||
on: { | ||
TYPING: [ | ||
{ | ||
target: 'idle', | ||
}, | ||
], | ||
SUBMIT: [ | ||
{ | ||
target: 'loading', | ||
cond: 'hasValue', | ||
}, | ||
{ | ||
target: 'error', | ||
}, | ||
], | ||
}, | ||
}, | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
<StateChart @src={{chart}} as |State Guard Action dispatch state|> | ||
{{yield (hash | ||
reset=(action dispatch "RESET") | ||
focus=(action 'focus') | ||
)}} | ||
<Guard @name="hasValue" @cond={{action 'hasValue'}} /> | ||
{{!FIXME: Call this reset or similar }} | ||
<Action @name="clearError" @exec={{queue (action (mut error) undefined) (action (mut secret) undefined)}} /> | ||
<div class="auth-form" ...attributes> | ||
<State @matches="error"> | ||
{{#if error.status}} | ||
<p role="alert" class="notice error"> | ||
{{#if value.Name}} | ||
{{#if (eq 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 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 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 /> | ||
{{error.detail}} | ||
{{/if}} | ||
{{else}} | ||
{{#if (eq error.status '403')}} | ||
<strong>Invalid token</strong><br /> | ||
The token entered does not exist. Please enter a valid token to log in. | ||
{{else}} | ||
<strong>Error</strong><br /> | ||
{{error.detail}} | ||
{{/if}} | ||
{{/if}} | ||
</p> | ||
{{/if}} | ||
</State> | ||
<form onsubmit={{action dispatch "SUBMIT"}}> | ||
<fieldset> | ||
<label class={{concat "type-password" (if (and (state-matches state 'error') (not error.status)) ' has-error' '')}}> | ||
<span>Log in with a token</span> | ||
<input | ||
{{ref this 'input'}} | ||
disabled={{state-matches state "loading"}} | ||
type="password" | ||
name="auth[SecretID]" | ||
placeholder="SecretID" | ||
value={{secret}} | ||
oninput={{queue | ||
(action (mut secret) value="target.value") | ||
(action (mut value) value="target.value") | ||
(action dispatch "TYPING") | ||
}} | ||
/> | ||
<State @matches="error"> | ||
{{#if (not error.status)}} | ||
<strong role="alert"> | ||
Please enter your secret | ||
</strong> | ||
{{/if}} | ||
</State> | ||
</label> | ||
</fieldset> | ||
<button type="submit" disabled={{state-matches state "loading"}}> | ||
Log in | ||
</button> | ||
<em>Contact your administrator for login credentials.</em> | ||
</form> | ||
{{#if (env 'CONSUL_SSO_ENABLED')}} | ||
<DataSource | ||
@src={{concat '/' (or nspace 'default') '/' dc '/oidc/providers'}} | ||
@onchange={{queue (action (mut providers) value="data")}} | ||
@onerror={{queue (action (mut error) value="error.errors.firstObject")}} | ||
@loading="lazy" | ||
/> | ||
{{#if (gt providers.length 0)}} | ||
<p> | ||
<span>or</span> | ||
</p> | ||
{{/if}} | ||
<OidcSelect | ||
@items={{providers}} | ||
@disabled={{state-matches state "loading"}} | ||
@onchange={{queue (action (mut value)) (action dispatch "SUBMIT") }} | ||
@onerror={{queue (action (mut error) value="error.errors.firstObject") (action dispatch "ERROR")}} | ||
/> | ||
{{/if}} | ||
</div> | ||
<State @matches="loading"> | ||
<TokenSource | ||
@dc={{dc}} | ||
@nspace={{nspace}} | ||
@type={{if value.Name 'oidc' 'secret'}} | ||
@value={{if value.Name value.Name value}} | ||
@onchange={{action onsubmit}} | ||
@onerror={{queue (action (mut error) value="error.errors.firstObject") (action dispatch "ERROR")}} | ||
/> | ||
</State> | ||
</StateChart> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import Component from '@ember/component'; | ||
|
||
import chart from './chart.xstate'; | ||
|
||
export default Component.extend({ | ||
tagName: '', | ||
onsubmit: function(e) {}, | ||
onchange: function(e) {}, | ||
init: function() { | ||
this._super(...arguments); | ||
this.chart = chart; | ||
}, | ||
actions: { | ||
hasValue: function(context, event, meta) { | ||
return this.value !== '' && typeof this.value !== 'undefined'; | ||
}, | ||
focus: function() { | ||
this.input.focus(); | ||
}, | ||
}, | ||
}); |
Oops, something went wrong.