ui: Upgrades token sourcing related components to Glimmer+docs (#11592)

This commit is contained in:
John Cowen 2021-11-18 15:52:39 +00:00 committed by GitHub
parent 764c22a6c4
commit 07e7fdcd09
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 264 additions and 104 deletions

View File

@ -1,27 +1,93 @@
---
class: ember
---
# JwtSource # JwtSource
```hbs This is a Source-like component and with most of our Source components, the
<JwtSource @src={{url}} @onchange={{action 'change'}} @onerror={{action 'error'}} /> component only needs to be added to the page in order to start the flow and is
meant to be used as such (think 'this is like an `<img />`).
The component will go through the steps of requesting a JWT token from a third
party OAuth provider. `src` should contain the full URL of the authorization
URL for the 3rd party provider including `redirect_uri`. Once the user has
logged into the 3rd party provider the `onchange` event will be fired
containing an event-like object whose data contains the JWT information.
During development you can use the `CONSUL_OIDC_PROVIDER_URL`
environment/cookie value to inject/mock the provider URL of your choice. This
var/cookie value **does not** need the `redirect_uri` parameter adding as that
will be automatically added by the UI . This URL will then be returned by our
mock API when requesting the AuthURL for **all** OIDC AuthMethods.
This component **does not store the resulting JWT**, it only emits it via
its `onchange` argument/event handler. Errors are emitted via the `onerror`
argument/event handler.
```hbs preview-template
<figure>
<figcaption>Provide a widget to set the URL</figcaption>
<input
oninput={{action (mut this.value) value="target.value"}}
type="text"
value={{this.value}}
{{did-insert (set this 'value'
(concat
(or
(env 'CONSUL_OIDC_PROVIDER_URL')
(concat (env 'CONSUL_BASE_UI_URL') '/oauth-provider-debug?client_id=oauth-double&nonce=1&state=123456789abc')
)
(concat '&redirect_uri=' (env 'CONSUL_BASE_UI_URL') '/oidc/callback&response_type=code&scope=openid')
)
)}}
/>
<button
type="button"
{{on 'click' (fn (mut this.state) 'authenticating')}}
>
Go
</button>
</figure>
<figure>
<figcaption>When the button is clicked add TokenSource to the page</figcaption>
{{#if (eq this.state 'authenticating')}}
<JwtSource
@src={{this.value}}
@onchange={{queue (action (mut this.jwt) value="data") (fn (mut this.state) '')}}
@onerror={{queue (action (mut this.error) value="error.errors.firstObject") (fn (mut this.state) '')}}
/>
{{/if}}
</figure>
<figure>
<figcaption>Show the results</figcaption>
<dl>
{{#if this.jwt}}
<dt>authorizationState</dt>
<dd>{{this.jwt.authorizationState}}</dd>
<dt>authorizationCode</dt>
<dd>{{this.jwt.authorizationCode}}</dd>
<dt>provider</dt>
<dd>{{this.jwt.provider}}</dd>
{{/if}}
{{#if this.error}}
<dt>Error</dt>
<dd>{{this.error.detail}}</dd>
{{/if}}
</dl>
</figure>
``` ```
### Arguments ## Arguments
| Argument | Type | Default | Description | | Argument | Type | Default | Description |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| `src` | `String` | | The source to subscribe to updates to, this should map to a string based URI | | `src` | `String` | | The **full** AuthURL for the 3rd party OIDC provider as provided by Consul (including `redirect_uri`) |
| `onchange` | `Function` | | The action to fire when the data changes. Emits an Event-like object with a `data` property containing the jwt data, in this case the autorizationCode and the status | | `onchange` | `Function` | | The action to fire when the data changes. Emits an Event-like object with a `data` property containing the JWT data, in this case the `autorizationStatus`, `autorizationCode`, plus the UI added `provider` name |
| `onerror` | `Function` | | The action to fire when an error occurs. Emits ErrorEvent object with an `error` property containing the Error. | | `onerror` | `Function` | | The action to fire when an error occurs. Emits ErrorEvent object with an `error` property containing the Error. |
This component will go through the steps of requesting a JWT token from a third party oauth provider. `src` should contain the full URL of the authorization URL for the 3rd party provider. Once the user has logged into the 3rd party provider the `onchange` event will be fired containing an event-like object whose data contains the JWT information.
The component need only be place into the DOM in order to begin the OAuth dance. ## See
### See
- [Component Source Code](./index.js) - [Component Source Code](./index.js)
- [Template Source Code](./index.hbs)
--- ---

View File

@ -1,31 +1,46 @@
import Component from '@ember/component'; import Component from '@glimmer/component';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import { fromPromise } from 'consul-ui/utils/dom/event-source'; import { fromPromise } from 'consul-ui/utils/dom/event-source';
export default Component.extend({ // TODO: We could probably update this to be a template only component now
repo: service('repository/oidc-provider'), // rather than a JS only one.
dom: service('dom'), export default class JWTSource extends Component {
tagName: '', @service('repository/oidc-provider') repo;
onchange: function(e) {}, @service('dom') dom;
onerror: function(e) {},
init: function() { constructor() {
this._super(...arguments); super(...arguments);
this._listeners = this.dom.listeners();
},
willDestroyElement: function() {
this._super(...arguments);
this.repo.close();
this._listeners.remove();
},
didInsertElement: function() {
if (this.source) { if (this.source) {
this.source.close(); this.source.close();
} }
this._listeners = this.dom.listeners();
// TODO: Could this use once? Double check but I don't think it can // TODO: Could this use once? Double check but I don't think it can
this.source = fromPromise(this.repo.findCodeByURL(this.src)); this.source = fromPromise(this.repo.findCodeByURL(this.args.src));
this._listeners.add(this.source, { this._listeners.add(this.source, {
message: e => this.onchange(e), message: e => this.onchange(e),
error: e => this.onerror(e), error: e => this.onerror(e),
}); });
}, }
});
onchange(e) {
if(typeof this.args.onchange === 'function') {
this.args.onchange(...arguments);
}
}
onerror(e) {
if(typeof this.args.onerror === 'function') {
this.args.onerror(...arguments);
}
}
willDestroy() {
super.willDestroy(...arguments);
if (this.source) {
this.source.close();
}
this.repo.close();
this._listeners.remove();
}
}

View File

@ -1,36 +1,89 @@
--- # TokenSource
class: ember
---
## TokenSource
```hbs This is a Source-like component that goes through either traditional 'secret'
<TokenSource based login or a 'oidc' flow depending on the `type` argument given.
@dc={{dc}}
@nspace={{nspace}} As with most of our Source components, the component only needs to be added to
@partition={{partition}} the page in order to start the flow and is meant to be used as such (think
@type={{or 'oidc' 'secret'}} 'this is like an `<img />`). It is also loosely based on a HTML `<input />`
@value={{or identifierForProvider secret}} element (it has `type` and `value` arguments/attributes)
@onchange={{action 'change'}}
@onerror={{action 'error'}} When using the `oidc` type the component will go through the steps of
requesting the OIDC providers authorization URL from Consul, go through the
OIDC flow with the user and the 3rd party provider (via our `<JwtSource />`
component), then lastly exchanging the resulting JWT with Consul for a normal
Consul token.
When using the `secret` type, the component simply exchanges the users secret
for a normal Consul token.
This component **does not store the resulting token**, it only emits it via
its `onchange` argument/event handler. Errors are emitted via the `onerror`
argument/event handler.
```hbs preview-template
<figure>
<figcaption>Provide a widget to login with</figcaption>
<input
oninput={{action (mut this.value) value="target.value"}}
type="text"
/> />
<button
type="button"
{{on 'click' (fn (mut this.state) 'authenticating')}}
>
Login
</button>
</figure>
<figure>
<figcaption>When the button is clicked add TokenSource to the page</figcaption>
{{#if (eq this.state 'authenticating')}}
<TokenSource
@dc={{'dc-1'}}
@nspace={{''}}
@partition={{''}}
@type={{or 'secret' 'oidc'}}
@value={{this.value}}
@onchange={{queue (action (mut this.token) value="data") (fn (mut this.state) '')}}
@onerror={{queue (action (mut this.error) value="error.errors.firstObject") (fn (mut this.state) '')}}
/>
{{/if}}
</figure>
<figure>
<figcaption>Show the results</figcaption>
<dl>
{{#if this.token}}
<dt>AccessorID</dt>
<dd>{{this.token.AccessorID}}</dd>
{{/if}}
{{#if this.error}}
<dt>Error</dt>
<dd>{{this.error.detail}}</dd>
{{/if}}
</dl>
</figure>
``` ```
### Arguments ## Arguments
| Argument | Type | Default | Description | | Argument | Type | Default | Description |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| `dc` | `String` | | The name of the current datacenter | | `dc` | `String` | | The name of the current datacenter |
| `nspace` | `String` | | The name of the current namespace | | `nspace` | `String` | | The name of the current namespace |
| `partition` | `String` | | The name of the current partition | | `partition` | `String` | | The name of the current partition |
| `type` | `String` | | `secret` or `oidc`. `secret` is just traditional login, whereas `oidc` uses the users OIDC provider |
| `value` | `String` | | When `type` is `secret` this should be the users secret. When `type` is `oidc` this should be the name of the `AuthMethod` to use for authentication |
| `onchange` | `Function` | | The action to fire when the data changes. Emits an Event-like object with a `data` property containing the jwt data, in this case the autorizationCode and the status | | `onchange` | `Function` | | The action to fire when the data changes. Emits an Event-like object with a `data` property containing the jwt data, in this case the autorizationCode and the status |
| `onerror` | `Function` | | The action to fire when an error occurs. Emits ErrorEvent object with an `error` property containing the Error. | | `onerror` | `Function` | | The action to fire when an error occurs. Emits ErrorEvent object with an `error` property containing the Error. |
This component will go through the steps of requesting a JWT token from a third party oauth provider. `src` should contain the full URL of the authorization URL for the 3rd party provider. Once the user has logged into the 3rd party provider the `onchange` event will be fired containing an event-like object whose data contains the JWT information.
The component need only be place into the DOM in order to begin the OAuth dance. ## See
### See
- [JwtSource Component](../jwt-source/README.mdx)
- [StateChart](./chart.xstate.js)
- [Component Source Code](./index.js) - [Component Source Code](./index.js)
- [Template Source Code](./index.hbs) - [Template Source Code](./index.hbs)

View File

@ -1,44 +1,60 @@
<StateChart @src={{chart}} @initial={{if (eq type 'oidc') 'provider' 'secret'}} as |State Guard Action dispatch state|> <StateChart
<Guard @name="isSecret" @cond={{action 'isSecret'}} /> @src={{this.chart}}
{{!-- This `or` can be completely removed post 1.10 as 1.10 has optional params with default values --}} @initial={{if (eq @type 'oidc') 'provider' 'secret'}}
{{#let (concat '/' (or partition '') '/' (or nspace '') '/' dc) as |path|}} as |State Guard Action dispatch state|>
<Guard
@name="isSecret"
@cond={{this.isSecret}}
/>
{{#let
(uri '/${partition}/{$nspace}/${dc}'
(hash
partition=@partition
nspace=@nspace
dc=@dc
)
)
as |path|}}
<State @matches="secret"> <State @matches="secret">
<DataSource <DataSource
@src={{uri (concat path '/token/self/${value}') @src={{uri (concat path '/token/self/${value}')
(hash (hash
value=value value=@value
) )
}} }}
@onchange={{action 'change'}} @onchange={{this.change}}
@onerror={{action onerror}} @onerror={{@onerror}}
/> />
</State> </State>
<State @matches="provider"> <State @matches="provider">
<DataSource <DataSource
@src={{concat path '/oidc/provider/' value}} @src={{uri (concat path '/oidc/provider/${value}')
@onchange={{queue (action (mut provider) value="data") (action dispatch "SUCCESS")}} (hash
@onerror={{action onerror}} value=@value
)
}}
@onchange={{queue (action (mut this.provider) value="data") (action dispatch "SUCCESS")}}
@onerror={{@onerror}}
/> />
</State> </State>
<State @matches="jwt"> <State @matches="jwt">
<JwtSource <JwtSource
@src={{provider.AuthURL}} @src={{this.provider.AuthURL}}
@onchange={{queue (action (mut jwt) value="data") (action dispatch "SUCCESS")}} @onchange={{queue (action (mut this.jwt) value="data") (action dispatch "SUCCESS")}}
@onerror={{action onerror}} @onerror={{@onerror}}
/> />
</State> </State>
<State @matches="token"> <State @matches="token">
<DataSource <DataSource
@src={{uri @src={{uri (concat path '/oidc/authorize/${provider}/${code}/${state}')
(concat path '/oidc/authorize/${provider}/${code}/${state}')
(hash (hash
provider=provider.Name provider=this.provider.Name
code=jwt.authorizationCode code=this.jwt.authorizationCode
state=(or jwt.authorizationState '') state=(or this.jwt.authorizationState '')
) )
}} }}
@onchange={{action 'change'}} @onchange={{this.change}}
@onerror={{action onerror}} @onerror={{@onerror}}
/> />
</State> </State>
{{/let}} {{/let}}

View File

@ -1,37 +1,47 @@
import Component from '@ember/component'; import Component from '@glimmer/component';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import chart from './chart.xstate'; import chart from './chart.xstate';
export default Component.extend({
onchange: function() {}, export default class TokenSource extends Component {
init: function() { @tracked provider;
this._super(...arguments); @tracked jwt;
constructor() {
super(...arguments);
this.chart = chart; this.chart = chart;
}, }
actions: {
isSecret: function() { @action
return this.type === 'secret'; isSecret() {
}, return this.args.type === 'secret';
change: function(e) { }
e.data.toJSON = function() {
return { @action
AccessorID: this.AccessorID, change(e) {
// TODO: In the past we've always ignored the SecretID returned e.data.toJSON = function() {
// from the server and used what the user typed in instead, now return {
// as we don't know the SecretID when we use SSO we use the SecretID AccessorID: this.AccessorID,
// in the response // TODO: In the past we've always ignored the SecretID returned
SecretID: this.SecretID, // from the server and used what the user typed in instead, now
Namespace: this.Namespace, // as we don't know the SecretID when we use SSO we use the SecretID
Partition: this.Partition, // in the response
...{ SecretID: this.SecretID,
AuthMethod: typeof this.AuthMethod !== 'undefined' ? this.AuthMethod : undefined, Namespace: this.Namespace,
// TODO: We should be able to only set namespaces if they are enabled Partition: this.Partition,
// but we might be testing for nspaces everywhere ...{
// Namespace: typeof this.Namespace !== 'undefined' ? this.Namespace : undefined AuthMethod: typeof this.AuthMethod !== 'undefined' ? this.AuthMethod : undefined,
}, // TODO: We should be able to only set namespaces if they are enabled
}; // but we might be testing for nspaces everywhere
// Namespace: typeof this.Namespace !== 'undefined' ? this.Namespace : undefined
},
}; };
// TODO: We should probably put the component into idle state };
this.onchange(e); // TODO: We should probably put the component into idle state
}, if(typeof this.args.onchange === 'function') {
}, this.args.onchange(e);
}); }
}
}