diff --git a/ui/packages/consul-ui/app/adapters/auth-method.js b/ui/packages/consul-ui/app/adapters/auth-method.js new file mode 100644 index 0000000000..fdfee01cea --- /dev/null +++ b/ui/packages/consul-ui/app/adapters/auth-method.js @@ -0,0 +1,28 @@ +import Adapter from './application'; + +export default class AuthMethodAdapter extends Adapter { + requestForQuery(request, { dc, ns, index, id }) { + return request` + GET /v1/acl/auth-methods?${{ dc }} + + ${{ + ...this.formatNspace(ns), + index, + }} + `; + } + + requestForQueryRecord(request, { dc, ns, index, id }) { + if (typeof id === 'undefined') { + throw new Error('You must specify an id'); + } + return request` + GET /v1/acl/auth-method/${id}?${{ dc }} + + ${{ + ...this.formatNspace(ns), + index, + }} + `; + } +} diff --git a/ui/packages/consul-ui/app/components/consul/auth-method/index.scss b/ui/packages/consul-ui/app/components/consul/auth-method/index.scss new file mode 100644 index 0000000000..3aef2415ff --- /dev/null +++ b/ui/packages/consul-ui/app/components/consul/auth-method/index.scss @@ -0,0 +1,9 @@ +.consul-auth-method-list ul { + .consul-auth-method-type { + @extend %pill-200, %frame-gray-600; + } + .locality::before { + @extend %with-public-default-mask, %as-pseudo; + margin-right: 4px; + } +} \ No newline at end of file diff --git a/ui/packages/consul-ui/app/components/consul/auth-method/list/index.hbs b/ui/packages/consul-ui/app/components/consul/auth-method/list/index.hbs new file mode 100644 index 0000000000..4b0223f1f5 --- /dev/null +++ b/ui/packages/consul-ui/app/components/consul/auth-method/list/index.hbs @@ -0,0 +1,31 @@ + + + {{#if (not-eq item.DisplayName '')}} +

{{item.DisplayName}}

+ {{else}} +

{{item.Name}}

+ {{/if}} +
+ + + {{#if (not-eq item.DisplayName '')}} + {{item.Name}} + {{/if}} + {{#if (eq item.TokenLocality 'global')}} + creates global tokens + {{/if}} + {{#if item.MaxTokenTTL}} +
+
+ + Maximum Time to Live: the maximum life of any token created by this auth method + +
+
{{item.MaxTokenTTL}}
+
+ {{/if}} +
+
diff --git a/ui/packages/consul-ui/app/components/consul/auth-method/list/pageobject.js b/ui/packages/consul-ui/app/components/consul/auth-method/list/pageobject.js new file mode 100644 index 0000000000..870bb9a889 --- /dev/null +++ b/ui/packages/consul-ui/app/components/consul/auth-method/list/pageobject.js @@ -0,0 +1,7 @@ +export default (collection, text) => () => { + return collection('.consul-auth-method-list [data-test-list-row]', { + name: text('[data-test-auth-method]'), + displayName: text('[data-test-display-name]'), + type: text('[data-test-type]'), + }); +}; diff --git a/ui/packages/consul-ui/app/components/consul/auth-method/search-bar/index.hbs b/ui/packages/consul-ui/app/components/consul/auth-method/search-bar/index.hbs new file mode 100644 index 0000000000..2e7e5313fa --- /dev/null +++ b/ui/packages/consul-ui/app/components/consul/auth-method/search-bar/index.hbs @@ -0,0 +1,148 @@ + + <:status as |search|> + +{{#let + + (t (concat "components.consul.auth-method.search-bar." search.status.key ".name") + default=(array + (concat "common.search." search.status.key) + (concat "common.consul." search.status.key) + ) + ) + + (t (concat "components.consul.auth-method.search-bar." search.status.key ".options." search.status.value) + default=(array + (concat "common.search." search.status.value) + (concat "common.consul." search.status.value) + (concat "common.brand." search.status.value) + ) + ) + +as |key value|}} + +
+
{{key}}
+
{{value}}
+
+
+{{/let}} + + + <:search as |search|> + + + + + {{t "common.search.searchproperty"}} + + + + {{#let components.Optgroup components.Option as |Optgroup Option|}} + {{#each @filter.searchproperty.default as |prop|}} + + {{/each}} + {{/let}} + + + + + <:filter as |search|> + + + + {{t "components.consul.auth-method.search-bar.kind.name"}} + + + + {{#let components.Optgroup components.Option as |Optgroup Option|}} + + + {{#if (env 'CONSUL_SSO_ENABLED')}} + + {{/if}} + {{/let}} + + + + + + {{t "components.consul.auth-method.search-bar.locality.name"}} + + + + {{#let components.Optgroup components.Option as |Optgroup Option|}} + {{#each (array "local" "global") as |option|}} + + {{/each}} + {{/let}} + + + + <:sort as |search|> + + + + {{#let (from-entries (array + (array "MethodName:asc" (t "common.sort.alpha.asc")) + (array "MethodName:desc" (t "common.sort.alpha.desc")) + (array "MaxTokenTTL:desc" (t "common.sort.duration.asc")) + (array "MaxTokenTTL:asc" (t "common.sort.duration.desc")) + )) + as |selectable| + }} + {{get selectable @sort.value}} + {{/let}} + + + + {{#let components.Optgroup components.Option as |Optgroup Option|}} + + + + + + + + + {{/let}} + + + +
diff --git a/ui/packages/consul-ui/app/components/consul/auth-method/type/index.hbs b/ui/packages/consul-ui/app/components/consul/auth-method/type/index.hbs new file mode 100644 index 0000000000..6aadacc3ca --- /dev/null +++ b/ui/packages/consul-ui/app/components/consul/auth-method/type/index.hbs @@ -0,0 +1,3 @@ + + {{t (concat "common.brand." @item.Type)}} + diff --git a/ui/packages/consul-ui/app/components/hashicorp-consul/index.hbs b/ui/packages/consul-ui/app/components/hashicorp-consul/index.hbs index 83b097c39c..bac2214fed 100644 --- a/ui/packages/consul-ui/app/components/hashicorp-consul/index.hbs +++ b/ui/packages/consul-ui/app/components/hashicorp-consul/index.hbs @@ -126,6 +126,9 @@
  • Roles
  • +
  • + Auth Methods +
  • {{/if}} diff --git a/ui/packages/consul-ui/app/filter/predicates/auth-method.js b/ui/packages/consul-ui/app/filter/predicates/auth-method.js new file mode 100644 index 0000000000..796cd23c6d --- /dev/null +++ b/ui/packages/consul-ui/app/filter/predicates/auth-method.js @@ -0,0 +1,11 @@ +export default { + kind: { + kubernetes: (item, value) => item.Type === value, + jwt: (item, value) => item.Type === value, + oidc: (item, value) => item.Type === value, + }, + source: { + local: (item, value) => item.TokenLocality === value, + global: (item, value) => item.TokenLocality === value, + }, +}; diff --git a/ui/packages/consul-ui/app/instance-initializers/nspace.js b/ui/packages/consul-ui/app/instance-initializers/nspace.js index 8bd6938413..0285415ae7 100644 --- a/ui/packages/consul-ui/app/instance-initializers/nspace.js +++ b/ui/packages/consul-ui/app/instance-initializers/nspace.js @@ -99,7 +99,10 @@ export function initialize(container) { register(container, index, indexed); } } - register(container, route, item); + + if (typeof route !== 'undefined') { + register(container, route, item); + } }); } } diff --git a/ui/packages/consul-ui/app/models/auth-method.js b/ui/packages/consul-ui/app/models/auth-method.js new file mode 100644 index 0000000000..a2aaf6c792 --- /dev/null +++ b/ui/packages/consul-ui/app/models/auth-method.js @@ -0,0 +1,24 @@ +import Model, { attr } from '@ember-data/model'; +import { or } from '@ember/object/computed'; + +export const PRIMARY_KEY = 'uid'; +export const SLUG_KEY = 'Name'; + +export default class AuthMethod extends Model { + @attr('string') uid; + @attr('string') Name; + + @attr('string') Datacenter; + @attr('string') Namespace; + @attr('string', { defaultValue: () => '' }) Description; + @attr('string', { defaultValue: () => '' }) DisplayName; + @attr('string', { defaultValue: () => 'local' }) TokenLocality; + @attr('string') Type; + @or('DisplayName', 'Name') MethodName; + @attr() Config; + @attr('string') MaxTokenTTL; + @attr('number') CreateIndex; + @attr('number') ModifyIndex; + @attr() Datacenters; // string[] + @attr() meta; // {} +} diff --git a/ui/packages/consul-ui/app/router.js b/ui/packages/consul-ui/app/router.js index a85c772475..f931be9631 100644 --- a/ui/packages/consul-ui/app/router.js +++ b/ui/packages/consul-ui/app/router.js @@ -149,6 +149,12 @@ export const routes = { _options: { path: '/create' }, }, }, + 'auth-methods': { + _options: { path: '/auth-methods' }, + show: { + _options: { path: '/show' }, + }, + }, }, }, // Shows a datacenter picker. If you only have one diff --git a/ui/packages/consul-ui/app/routes/dc/acls/auth-methods/index.js b/ui/packages/consul-ui/app/routes/dc/acls/auth-methods/index.js new file mode 100644 index 0000000000..f45d92397b --- /dev/null +++ b/ui/packages/consul-ui/app/routes/dc/acls/auth-methods/index.js @@ -0,0 +1,38 @@ +import { inject as service } from '@ember/service'; +import Route from 'consul-ui/routing/route'; +import { hash } from 'rsvp'; + +export default class IndexRoute extends Route { + @service('repository/auth-method') repo; + + queryParams = { + sortBy: 'sort', + source: 'source', + kind: 'kind', + searchproperty: { + as: 'searchproperty', + empty: [['Name', 'DisplayName']], + }, + search: { + as: 'filter', + replace: true, + }, + }; + + model(params) { + return hash({ + ...this.repo.status({ + items: this.repo.findAllByDatacenter( + this.modelFor('dc').dc.Name, + this.modelFor('nspace').nspace.substr(1) + ), + }), + searchProperties: this.queryParams.searchproperty.empty[0], + }); + } + + setupController(controller, model) { + super.setupController(...arguments); + controller.setProperties(model); + } +} diff --git a/ui/packages/consul-ui/app/search/predicates/auth-method.js b/ui/packages/consul-ui/app/search/predicates/auth-method.js new file mode 100644 index 0000000000..15a6cf3514 --- /dev/null +++ b/ui/packages/consul-ui/app/search/predicates/auth-method.js @@ -0,0 +1,4 @@ +export default { + Name: item => item.Name, + DisplayName: item => item.DisplayName, +}; diff --git a/ui/packages/consul-ui/app/serializers/auth-method.js b/ui/packages/consul-ui/app/serializers/auth-method.js new file mode 100644 index 0000000000..435382c684 --- /dev/null +++ b/ui/packages/consul-ui/app/serializers/auth-method.js @@ -0,0 +1,7 @@ +import Serializer from './application'; +import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/auth-method'; + +export default class AuthMethodSerializer extends Serializer { + primaryKey = PRIMARY_KEY; + slugKey = SLUG_KEY; +} diff --git a/ui/packages/consul-ui/app/services/filter.js b/ui/packages/consul-ui/app/services/filter.js index 89b2cf8692..7a521f9e16 100644 --- a/ui/packages/consul-ui/app/services/filter.js +++ b/ui/packages/consul-ui/app/services/filter.js @@ -10,12 +10,14 @@ import kv from 'consul-ui/filter/predicates/kv'; import intention from 'consul-ui/filter/predicates/intention'; import token from 'consul-ui/filter/predicates/token'; import policy from 'consul-ui/filter/predicates/policy'; +import authMethod from 'consul-ui/filter/predicates/auth-method'; const predicates = { acl: andOr(acl), service: andOr(service), ['service-instance']: andOr(serviceInstance), ['health-check']: andOr(healthCheck), + ['auth-method']: andOr(authMethod), node: andOr(node), kv: andOr(kv), intention: andOr(intention), diff --git a/ui/packages/consul-ui/app/services/repository/auth-method.js b/ui/packages/consul-ui/app/services/repository/auth-method.js new file mode 100644 index 0000000000..818b3ecacb --- /dev/null +++ b/ui/packages/consul-ui/app/services/repository/auth-method.js @@ -0,0 +1,26 @@ +import RepositoryService from 'consul-ui/services/repository'; +import statusFactory from 'consul-ui/utils/acls-status'; +import isValidServerErrorFactory from 'consul-ui/utils/http/acl/is-valid-server-error'; +import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/auth-method'; + +const isValidServerError = isValidServerErrorFactory(); +const status = statusFactory(isValidServerError, Promise); +const MODEL_NAME = 'auth-method'; + +export default class AuthMethodService extends RepositoryService { + getModelName() { + return MODEL_NAME; + } + + getPrimaryKey() { + return PRIMARY_KEY; + } + + getSlugKey() { + return SLUG_KEY; + } + + status(obj) { + return status(obj); + } +} diff --git a/ui/packages/consul-ui/app/services/search.js b/ui/packages/consul-ui/app/services/search.js index 086bd43266..dce0d897a6 100644 --- a/ui/packages/consul-ui/app/services/search.js +++ b/ui/packages/consul-ui/app/services/search.js @@ -13,6 +13,7 @@ import kv from 'consul-ui/search/predicates/kv'; import token from 'consul-ui/search/predicates/token'; import role from 'consul-ui/search/predicates/role'; import policy from 'consul-ui/search/predicates/policy'; +import authMethod from 'consul-ui/search/predicates/auth-method'; import nspace from 'consul-ui/search/predicates/nspace'; const predicates = { @@ -21,6 +22,7 @@ const predicates = { ['service-instance']: serviceInstance, ['upstream-instance']: upstreamInstance, ['health-check']: healthCheck, + ['auth-method']: authMethod, node: node, kv: kv, acl: acl, diff --git a/ui/packages/consul-ui/app/services/sort.js b/ui/packages/consul-ui/app/services/sort.js index dd63993900..44ff5aac7b 100644 --- a/ui/packages/consul-ui/app/services/sort.js +++ b/ui/packages/consul-ui/app/services/sort.js @@ -9,6 +9,7 @@ import intention from 'consul-ui/sort/comparators/intention'; import token from 'consul-ui/sort/comparators/token'; import role from 'consul-ui/sort/comparators/role'; import policy from 'consul-ui/sort/comparators/policy'; +import authMethod from 'consul-ui/sort/comparators/auth-method'; import nspace from 'consul-ui/sort/comparators/nspace'; import node from 'consul-ui/sort/comparators/node'; @@ -32,6 +33,7 @@ const comparators = { ['service-instance']: serviceInstance(options), ['upstream-instance']: upstreamInstance(options), ['health-check']: healthCheck(options), + ['auth-method']: authMethod(options), acl: acl(options), kv: kv(options), intention: intention(options), diff --git a/ui/packages/consul-ui/app/sort/comparators/auth-method.js b/ui/packages/consul-ui/app/sort/comparators/auth-method.js new file mode 100644 index 0000000000..fafacd3fff --- /dev/null +++ b/ui/packages/consul-ui/app/sort/comparators/auth-method.js @@ -0,0 +1,3 @@ +export default ({ properties }) => (key = 'MethodName:asc') => { + return properties(['MethodName', 'MaxTokenTTL'])(key); +}; diff --git a/ui/packages/consul-ui/app/styles/base/icons/base-variables.scss b/ui/packages/consul-ui/app/styles/base/icons/base-variables.scss index 35f6f25261..2dd1a649e7 100644 --- a/ui/packages/consul-ui/app/styles/base/icons/base-variables.scss +++ b/ui/packages/consul-ui/app/styles/base/icons/base-variables.scss @@ -77,6 +77,7 @@ $history-svg: url('data:image/svg+xml;charset=UTF-8,'); $info-circle-fill-svg: url('data:image/svg+xml;charset=UTF-8,'); $info-circle-outline-svg: url('data:image/svg+xml;charset=UTF-8,'); +$jwt-logo-svg: url('data:image/svg+xml;charset=UTF-8,'); $key-svg: url('data:image/svg+xml;charset=UTF-8,'); $kubernetes-logo-color-svg: url('data:image/svg+xml;charset=UTF-8,'); $layers-svg: url('data:image/svg+xml;charset=UTF-8,'); @@ -128,6 +129,7 @@ $nomad-logo-color-svg: url('data:image/svg+xml;charset=UTF-8,'); $notification-fill-svg: url('data:image/svg+xml;charset=UTF-8,'); $notification-outline-svg: url('data:image/svg+xml;charset=UTF-8,'); +$oidc-logo-svg: url('data:image/svg+xml;charset=UTF-8,'); $outline-svg: url('data:image/svg+xml;charset=UTF-8,'); $page-outline-svg: url('data:image/svg+xml;charset=UTF-8,'); $partner-svg: url('data:image/svg+xml;charset=UTF-8,'); diff --git a/ui/packages/consul-ui/app/styles/base/icons/icon-placeholders.scss b/ui/packages/consul-ui/app/styles/base/icons/icon-placeholders.scss index a497982733..15e4366d12 100644 --- a/ui/packages/consul-ui/app/styles/base/icons/icon-placeholders.scss +++ b/ui/packages/consul-ui/app/styles/base/icons/icon-placeholders.scss @@ -778,6 +778,16 @@ mask-image: $info-circle-outline-svg; } +%with-jwt-logo-icon { + @extend %with-icon; + background-image: $jwt-logo-svg; +} +%with-jwt-logo-mask { + @extend %with-mask; + -webkit-mask-image: $jwt-logo-svg; + mask-image: $jwt-logo-svg; +} + %with-key-icon { @extend %with-icon; background-image: $key-svg; @@ -1316,6 +1326,16 @@ mask-image: $notification-outline-svg; } +%with-oidc-logo-icon { + @extend %with-icon; + background-image: $oidc-logo-svg; +} +%with-oidc-logo-mask { + @extend %with-mask; + -webkit-mask-image: $oidc-logo-svg; + mask-image: $oidc-logo-svg; +} + %with-outline-icon { @extend %with-icon; background-image: $outline-svg; diff --git a/ui/packages/consul-ui/app/styles/components.scss b/ui/packages/consul-ui/app/styles/components.scss index 1a22481b48..51765dab26 100644 --- a/ui/packages/consul-ui/app/styles/components.scss +++ b/ui/packages/consul-ui/app/styles/components.scss @@ -74,6 +74,7 @@ @import 'consul-ui/components/consul/kind'; @import 'consul-ui/components/consul/intention'; @import 'consul-ui/components/consul/lock-session/form'; +@import 'consul-ui/components/consul/auth-method'; @import 'consul-ui/components/role-selector'; @import 'consul-ui/components/topology-metrics'; diff --git a/ui/packages/consul-ui/app/styles/components/pill.scss b/ui/packages/consul-ui/app/styles/components/pill.scss index 038a7e93e9..236eac8daf 100644 --- a/ui/packages/consul-ui/app/styles/components/pill.scss +++ b/ui/packages/consul-ui/app/styles/components/pill.scss @@ -33,3 +33,9 @@ span.policy-service-identity::before { %pill.leader::before { @extend %with-star-outline-mask, %as-pseudo; } +%pill.jwt::before { + @extend %with-jwt-logo-icon, %as-pseudo; +} +%pill.oidc::before { + @extend %with-oidc-logo-icon, %as-pseudo; +} diff --git a/ui/packages/consul-ui/app/styles/components/popover-select.scss b/ui/packages/consul-ui/app/styles/components/popover-select.scss index 6cad84b09c..6f7653da83 100644 --- a/ui/packages/consul-ui/app/styles/components/popover-select.scss +++ b/ui/packages/consul-ui/app/styles/components/popover-select.scss @@ -49,6 +49,12 @@ %popover-select .kubernetes button::before { @extend %with-logo-kubernetes-color-icon, %as-pseudo; } +%popover-select .jwt button::before { + @extend %with-jwt-logo-icon, %as-pseudo; +} +%popover-select .oidc button::before { + @extend %with-oidc-logo-icon, %as-pseudo; +} %popover-select .consul button::before { @extend %with-logo-consul-color-icon, %as-pseudo; } diff --git a/ui/packages/consul-ui/app/templates/dc/acls/auth-methods/index.hbs b/ui/packages/consul-ui/app/templates/dc/acls/auth-methods/index.hbs new file mode 100644 index 0000000000..95f507c8e1 --- /dev/null +++ b/ui/packages/consul-ui/app/templates/dc/acls/auth-methods/index.hbs @@ -0,0 +1,102 @@ +{{#if isAuthorized }} + {{page-title 'Auth Methods'}} +{{else}} + {{page-title 'Access Controls'}} +{{/if}} + +{{#let + + (hash + value=(or sortBy "MethodName:asc") + change=(action (mut sortBy) value="target.selected") + ) + + (hash + kind=(hash + value=(if kind (split kind ',') undefined) + change=(action (mut kind) value="target.selectedItems") + ) + source=(hash + value=(if source (split source ',') undefined) + change=(action (mut source) value="target.selectedItems") + ) + searchproperty=(hash + value=(if (not-eq searchproperty undefined) + (split searchproperty ',') + searchProperties + ) + change=(action (mut searchproperty) value="target.selectedItems") + default=searchProperties + ) + ) + + items + +as |sort filters items|}} + + + + +

    + Access Controls +

    +
    + + {{#if (gt items.length 0)}} + + {{/if}} + + + + + + + + + +

    + {{#if (gt items.length 0)}} + No auth methods found + {{else}} + Welcome to Auth Methods + {{/if}} +

    +
    + +

    + {{#if (gt items.length 0)}} + No auth methods where found matching that search, or you may not have access to view the auth methods you are searching for. + {{else}} + There don't seem to be any auth methods, or you may not have access to view auth methods yet. + {{/if}} +

    +
    + + + + +
    +
    +
    +
    +
    +{{/let}} + diff --git a/ui/packages/consul-ui/app/templates/dc/acls/roles/index.hbs b/ui/packages/consul-ui/app/templates/dc/acls/roles/index.hbs index aee311bdd3..af50974f1a 100644 --- a/ui/packages/consul-ui/app/templates/dc/acls/roles/index.hbs +++ b/ui/packages/consul-ui/app/templates/dc/acls/roles/index.hbs @@ -97,7 +97,7 @@ as |sort filters items|}} Documentation on roles diff --git a/ui/packages/consul-ui/mock-api/v1/acl/auth-method/_ b/ui/packages/consul-ui/mock-api/v1/acl/auth-method/_ new file mode 100644 index 0000000000..2cd545e4e3 --- /dev/null +++ b/ui/packages/consul-ui/mock-api/v1/acl/auth-method/_ @@ -0,0 +1,44 @@ +${ + [1].map(() => { + const type = `${fake.helpers.randomize(['kubernetes', 'jwt', 'oidc'])}`; + const fakeIP = `${fake.internet.ip()}`; + let config = {}; + switch(type) { + case 'kubernetes': + config = { + Host: `https://${fake.internet.ip()}:8443`, + CACert: `-----BEGIN CERTIFICATE-----${fake.internet.password(1357)}-----END CERTIFICATE-----`, + ServiceAccountJWT: `eyJhbGciOiJ${fake.internet.password(25)}.eyJ${fake.internet.password(61)}.${fake.internet.password(32)}` + }; + break; + case 'oidc': + config = { + OIDCDiscoveryURL: `https://${fake.internet.ip()}:8443`, + }; + break; + case 'jwt': + config = { + JWTValidationPubKeys: `-----BEGIN CERTIFICATE-----${fake.internet.password(1357)}-----END CERTIFICATE-----`, + JWKSURL: `https://${fake.internet.ip()}:8443`, + OIDCDiscoveryURL: `https://${fake.internet.ip()}:8443`, + }; + break; + } + + return `{ + "Name": "${location.pathname.get(3)}", + "Namespace": "${ + typeof location.search.ns !== 'undefined' ? location.search.ns : + typeof http.body.Namespace !== 'undefined' ? http.body.Namespace : 'default' + }", + "Type": "${type}", + "Description": "${fake.lorem.sentence()}", + "DisplayName": "${fake.hacker.noun()}", + "MaxTokenTTL": "${fake.random.number({min: 0, max: 60})}m${fake.random.number({min: 0, max: 60})}s", + "TokenLocality": "${fake.helpers.randomize(['local', 'global', ''])}", + "Config": ${JSON.stringify(config)}, + "CreateIndex": ${fake.random.number()}, + "ModifyIndex": 10 + }` + }) +} diff --git a/ui/packages/consul-ui/mock-api/v1/acl/auth-methods b/ui/packages/consul-ui/mock-api/v1/acl/auth-methods new file mode 100644 index 0000000000..d5697e5a05 --- /dev/null +++ b/ui/packages/consul-ui/mock-api/v1/acl/auth-methods @@ -0,0 +1,42 @@ +[ + ${ + range( + env( + 'CONSUL_AUTH_METHOD_COUNT', + Math.floor( + ( + Math.random() * env('CONSUL_AUTH_METHOD_MAX', 10) + ) + parseInt(env('CONSUL_AUTH_METHOD_MIN', 1)) + ) + ) + ).map( + function(item, i) { + return ` + { + "Name": "${fake.hacker.noun()}-${i}", +${typeof location.search.ns !== 'undefined' ? ` + "Namespace": "${location.search.ns}", +` : ``} + "Type": "${fake.helpers.randomize(['kubernetes', 'jwt', 'oidc'])}", + "Description": "${fake.lorem.sentence()}", +${i%2 ? ` + "DisplayName": "${fake.hacker.noun()}-${i}", +` : ` + "DisplayName": "", +`} +${i%2 ? ` + "MaxTokenTTL": "${fake.random.number({min: 0, max: 60})}m${fake.random.number({min: 0, max: 60})}s", +` : ` +`} +${i%2 ? ` + "TokenLocality": "${fake.helpers.randomize(['local', 'global', ''])}", +` : ` +`} + "CreateIndex": ${fake.random.number()}, + "ModifyIndex": 10 + } + ` + } + ) + } +] diff --git a/ui/packages/consul-ui/tests/acceptance/dc/acls/auth-methods/index.feature b/ui/packages/consul-ui/tests/acceptance/dc/acls/auth-methods/index.feature new file mode 100644 index 0000000000..68da2e61f7 --- /dev/null +++ b/ui/packages/consul-ui/tests/acceptance/dc/acls/auth-methods/index.feature @@ -0,0 +1,48 @@ +@setupApplicationTest +Feature: dc / acls / auth-methods / index: ACL Auth Methods List + + Scenario: + Given 1 datacenter model with the value "dc-1" + And 3 authMethod models + When I visit the authMethods page for yaml + --- + dc: dc-1 + --- + Then the url should be /dc-1/acls/auth-methods + Then I see 3 authMethod models + And the title should be "Auth Methods - Consul" + Scenario: Searching the Auth Methods + Given 1 datacenter model with the value "dc-1" + And 3 authMethod models from yaml + --- + - Name: kube + DisplayName: minikube + - Name: agent + DisplayName: '' + - Name: node + DisplayName: mininode + --- + When I visit the authMethods page for yaml + --- + dc: dc-1 + --- + Then the url should be /dc-1/acls/auth-methods + Then I see 3 authMethod models + Then I fill in with yaml + --- + s: kube + --- + And I see 1 authMethod model + And I see 1 authMethod model with the name "minikube" + Then I fill in with yaml + --- + s: agent + --- + And I see 1 authMethod model + And I see 1 authMethod model with the name "agent" + Then I fill in with yaml + --- + s: ode + --- + And I see 1 authMethod model + And I see 1 authMethod model with the name "mininode" diff --git a/ui/packages/consul-ui/tests/acceptance/dc/acls/auth-methods/sorting.feature b/ui/packages/consul-ui/tests/acceptance/dc/acls/auth-methods/sorting.feature new file mode 100644 index 0000000000..11cd0e4d89 --- /dev/null +++ b/ui/packages/consul-ui/tests/acceptance/dc/acls/auth-methods/sorting.feature @@ -0,0 +1,39 @@ +@setupApplicationTest +Feature: dc / acls / auth-methods / sorting + Scenario: Sorting Auth Methods + Given 1 datacenter model with the value "dc-1" + And 4 authMethod models from yaml + --- + - Name: "system-A" + DisplayName: '' + - Name: "system-D" + DisplayName: '' + - Name: "system-C" + DisplayName: '' + - Name: "system-B" + DisplayName: '' + --- + When I visit the authMethods page for yaml + --- + dc: dc-1 + --- + Then the url should be /dc-1/acls/auth-methods + Then I see 4 authMethod models + When I click selected on the sort + When I click options.1.button on the sort + Then I see name on the authMethods vertically like yaml + --- + - "system-D" + - "system-C" + - "system-B" + - "system-A" + --- + When I click selected on the sort + When I click options.0.button on the sort + Then I see name on the authMethods vertically like yaml + --- + - "system-A" + - "system-B" + - "system-C" + - "system-D" + --- diff --git a/ui/packages/consul-ui/tests/acceptance/steps/dc/acls/auth-methods/index-steps.js b/ui/packages/consul-ui/tests/acceptance/steps/dc/acls/auth-methods/index-steps.js new file mode 100644 index 0000000000..3231912b98 --- /dev/null +++ b/ui/packages/consul-ui/tests/acceptance/steps/dc/acls/auth-methods/index-steps.js @@ -0,0 +1,10 @@ +import steps from '../../../steps'; + +// step definitions that are shared between features should be moved to the +// tests/acceptance/steps/steps.js file + +export default function(assert) { + return steps(assert).then('I should find a file', function() { + assert.ok(true, this.step); + }); +} diff --git a/ui/packages/consul-ui/tests/acceptance/steps/dc/acls/auth-methods/sorting-steps.js b/ui/packages/consul-ui/tests/acceptance/steps/dc/acls/auth-methods/sorting-steps.js new file mode 100644 index 0000000000..3231912b98 --- /dev/null +++ b/ui/packages/consul-ui/tests/acceptance/steps/dc/acls/auth-methods/sorting-steps.js @@ -0,0 +1,10 @@ +import steps from '../../../steps'; + +// step definitions that are shared between features should be moved to the +// tests/acceptance/steps/steps.js file + +export default function(assert) { + return steps(assert).then('I should find a file', function() { + assert.ok(true, this.step); + }); +} diff --git a/ui/packages/consul-ui/tests/helpers/set-cookies.js b/ui/packages/consul-ui/tests/helpers/set-cookies.js index 53ff9872c8..772b8fe439 100644 --- a/ui/packages/consul-ui/tests/helpers/set-cookies.js +++ b/ui/packages/consul-ui/tests/helpers/set-cookies.js @@ -42,6 +42,10 @@ export default function(type, value) { key = 'CONSUL_TOKEN_COUNT'; obj['CONSUL_ACLS_ENABLE'] = 1; break; + case 'authMethod': + key = 'CONSUL_AUTH_METHOD_COUNT'; + obj['CONSUL_ACLS_ENABLE'] = 1; + break; case 'nspace': key = 'CONSUL_NSPACE_COUNT'; break; diff --git a/ui/packages/consul-ui/tests/helpers/type-to-url.js b/ui/packages/consul-ui/tests/helpers/type-to-url.js index 905480bee7..aa45c3fb31 100644 --- a/ui/packages/consul-ui/tests/helpers/type-to-url.js +++ b/ui/packages/consul-ui/tests/helpers/type-to-url.js @@ -35,6 +35,9 @@ export default function(type) { case 'token': requests = ['/v1/acl/tokens', '/v1/acl/token/']; break; + case 'authMethod': + requests = ['/v1/acl/auth-methods', '/v1/acl/auth-method/']; + break; case 'nspace': requests = ['/v1/namespaces', '/v1/namespace/']; break; diff --git a/ui/packages/consul-ui/tests/integration/adapters/auth-method-test.js b/ui/packages/consul-ui/tests/integration/adapters/auth-method-test.js new file mode 100644 index 0000000000..8c41801ac2 --- /dev/null +++ b/ui/packages/consul-ui/tests/integration/adapters/auth-method-test.js @@ -0,0 +1,50 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; +import getNspaceRunner from 'consul-ui/tests/helpers/get-nspace-runner'; + +const nspaceRunner = getNspaceRunner('auth-method'); +module('Integration | Adapter | auth-method', function(hooks) { + setupTest(hooks); + const dc = 'dc-1'; + const id = 'slug'; + test('requestForQueryRecord returns the correct url/method', function(assert) { + const adapter = this.owner.lookup('adapter:auth-method'); + const client = this.owner.lookup('service:client/http'); + const expected = `GET /v1/acl/auth-method/${id}?dc=${dc}`; + const actual = adapter.requestForQueryRecord(client.requestParams.bind(client), { + dc: dc, + id: id, + }); + assert.equal(`${actual.method} ${actual.url}`, expected); + }); + test("requestForQueryRecord throws if you don't specify an id", function(assert) { + const adapter = this.owner.lookup('adapter:auth-method'); + const client = this.owner.lookup('service:client/http'); + assert.throws(function() { + adapter.requestForQueryRecord(client.url, { + dc: dc, + }); + }); + }); + test('requestForQueryRecord returns the correct body', function(assert) { + return nspaceRunner( + (adapter, serializer, client) => { + return adapter.requestForQueryRecord(client.body, { + id: id, + dc: dc, + ns: 'team-1', + index: 1, + }); + }, + { + index: 1, + ns: 'team-1', + }, + { + index: 1, + }, + this, + assert + ); + }); +}); diff --git a/ui/packages/consul-ui/tests/integration/serializers/auth-method-test.js b/ui/packages/consul-ui/tests/integration/serializers/auth-method-test.js new file mode 100644 index 0000000000..002f662f19 --- /dev/null +++ b/ui/packages/consul-ui/tests/integration/serializers/auth-method-test.js @@ -0,0 +1,75 @@ +import { module, test, skip } from 'qunit'; +import { setupTest } from 'ember-qunit'; +import { get } from 'consul-ui/tests/helpers/api'; +import { + HEADERS_SYMBOL as META, + HEADERS_DATACENTER as DC, + HEADERS_NAMESPACE as NSPACE, +} from 'consul-ui/utils/http/consul'; +module('Integration | Serializer | auth-method', function(hooks) { + setupTest(hooks); + const dc = 'dc-1'; + const id = 'auth-method-name'; + const undefinedNspace = 'default'; + [undefinedNspace, 'team-1', undefined].forEach(nspace => { + test(`respondForQuery returns the correct data for list endpoint when nspace is ${nspace}`, function(assert) { + const serializer = this.owner.lookup('serializer:auth-method'); + const request = { + url: `/v1/acl/auth-methods?dc=${dc}${typeof nspace !== 'undefined' ? `&ns=${nspace}` : ``}`, + }; + return get(request.url).then(function(payload) { + const expected = payload.map(item => + Object.assign({}, item, { + Datacenter: dc, + Namespace: item.Namespace || undefinedNspace, + uid: `["${item.Namespace || undefinedNspace}","${dc}","${item.Name}"]`, + }) + ); + const actual = serializer.respondForQuery( + function(cb) { + const headers = {}; + const body = payload; + return cb(headers, body); + }, + { + dc: dc, + ns: nspace, + } + ); + assert.deepEqual(actual, expected); + }); + }); + skip(`respondForQueryRecord returns the correct data for item endpoint when nspace is ${nspace}`, function(assert) { + const serializer = this.owner.lookup('serializer:auth-method'); + const request = { + url: `/v1/acl/auth-method/${id}?dc=${dc}${ + typeof nspace !== 'undefined' ? `&ns=${nspace}` : `` + }`, + }; + return get(request.url).then(function(payload) { + const expected = Object.assign({}, payload, { + Datacenter: dc, + [META]: { + [DC.toLowerCase()]: dc, + [NSPACE.toLowerCase()]: payload.Namespace || undefinedNspace, + }, + Namespace: payload.Namespace || undefinedNspace, + uid: `["${payload.Namespace || undefinedNspace}","${dc}","${id}"]`, + }); + const actual = serializer.respondForQueryRecord( + function(cb) { + const headers = {}; + const body = payload; + return cb(headers, body); + }, + { + dc: dc, + ns: nspace, + id: id, + } + ); + assert.deepEqual(actual, expected); + }); + }); + }); +}); diff --git a/ui/packages/consul-ui/tests/integration/services/repository/auth-method-test.js b/ui/packages/consul-ui/tests/integration/services/repository/auth-method-test.js new file mode 100644 index 0000000000..2d489be3a6 --- /dev/null +++ b/ui/packages/consul-ui/tests/integration/services/repository/auth-method-test.js @@ -0,0 +1,82 @@ +import { moduleFor, test } from 'ember-qunit'; +import repo from 'consul-ui/tests/helpers/repo'; +import { skip } from 'qunit'; + +const NAME = 'auth-method'; +moduleFor(`service:repository/${NAME}`, `Integration | Service | ${NAME}`, { + // Specify the other units that are required for this test. + integration: true, +}); +const dc = 'dc-1'; +const id = 'auth-method-name'; +const undefinedNspace = 'default'; +[undefinedNspace, 'team-1', undefined].forEach(nspace => { + test(`findAllByDatacenter returns the correct data for list endpoint when nspace is ${nspace}`, function(assert) { + return repo( + 'auth-method', + 'findAllByDatacenter', + this.subject(), + function retrieveStub(stub) { + return stub( + `/v1/acl/auth-methods?dc=${dc}${typeof nspace !== 'undefined' ? `&ns=${nspace}` : ``}`, + { + CONSUL_AUTH_METHOD_COUNT: '3', + } + ); + }, + function performTest(service) { + return service.findAllByDatacenter(dc, nspace || undefinedNspace); + }, + function performAssertion(actual, expected) { + assert.deepEqual( + actual, + expected(function(payload) { + return payload.map(function(item) { + return Object.assign({}, item, { + Datacenter: dc, + Namespace: item.Namespace || undefinedNspace, + uid: `["${item.Namespace || undefinedNspace}","${dc}","${item.Name}"]`, + }); + }); + }) + ); + } + ); + }); + skip(`findBySlug returns the correct data for item endpoint when the nspace is ${nspace}`, function(assert) { + return repo( + 'AuthMethod', + 'findBySlug', + this.subject(), + function retrieveStub(stub) { + return stub( + `/v1/acl/auth-method/${id}?dc=${dc}${ + typeof nspace !== 'undefined' ? `&ns=${nspace}` : `` + }` + ); + }, + function performTest(service) { + return service.findBySlug(id, dc, nspace || undefinedNspace); + }, + function performAssertion(actual, expected) { + assert.deepEqual( + actual, + expected(function(payload) { + const item = payload; + return Object.assign({}, item, { + Datacenter: dc, + Namespace: item.Namespace || undefinedNspace, + uid: `["${item.Namespace || undefinedNspace}","${dc}","${item.Name}"]`, + meta: { + cacheControl: undefined, + cursor: undefined, + dc: dc, + nspace: item.Namespace || undefinedNspace, + }, + }); + }) + ); + } + ); + }); +}); diff --git a/ui/packages/consul-ui/tests/pages.js b/ui/packages/consul-ui/tests/pages.js index 3512763071..26d7f00ce7 100644 --- a/ui/packages/consul-ui/tests/pages.js +++ b/ui/packages/consul-ui/tests/pages.js @@ -42,6 +42,7 @@ import consulUpstreamInstanceListFactory from 'consul-ui/components/consul/upstr import consulTokenListFactory from 'consul-ui/components/consul/token/list/pageobject'; import consulRoleListFactory from 'consul-ui/components/consul/role/list/pageobject'; import consulPolicyListFactory from 'consul-ui/components/consul/policy/list/pageobject'; +import consulAuthMethodListFactory from 'consul-ui/components/consul/auth-method/list/pageobject'; import consulIntentionListFactory from 'consul-ui/components/consul/intention/list/pageobject'; import consulNspaceListFactory from 'consul-ui/components/consul/nspace/list/pageobject'; import consulKvListFactory from 'consul-ui/components/consul/kv/list/pageobject'; @@ -65,6 +66,7 @@ import roles from 'consul-ui/tests/pages/dc/acls/roles/index'; import role from 'consul-ui/tests/pages/dc/acls/roles/edit'; import tokens from 'consul-ui/tests/pages/dc/acls/tokens/index'; import token from 'consul-ui/tests/pages/dc/acls/tokens/edit'; +import authMethods from 'consul-ui/tests/pages/dc/acls/auth-methods/index'; import intentions from 'consul-ui/tests/pages/dc/intentions/index'; import intention from 'consul-ui/tests/pages/dc/intentions/edit'; import nspaces from 'consul-ui/tests/pages/dc/nspaces/index'; @@ -90,6 +92,7 @@ const emptyState = emptyStateFactory(isPresent); const consulHealthCheckList = consulHealthCheckListFactory(collection, text); const consulUpstreamInstanceList = consulUpstreamInstanceListFactory(collection, text); +const consulAuthMethodList = consulAuthMethodListFactory(collection, text); const consulIntentionList = consulIntentionListFactory( collection, clickable, @@ -191,6 +194,7 @@ export default { token: create( token(visitable, submitable, deletable, cancelable, clickable, policySelector, roleSelector) ), + authMethods: create(authMethods(visitable, creatable, consulAuthMethodList, popoverSelect)), intentions: create( intentions(visitable, creatable, clickable, consulIntentionList, popoverSelect) ), diff --git a/ui/packages/consul-ui/tests/pages/dc/acls/auth-methods/index.js b/ui/packages/consul-ui/tests/pages/dc/acls/auth-methods/index.js new file mode 100644 index 0000000000..030736cdcb --- /dev/null +++ b/ui/packages/consul-ui/tests/pages/dc/acls/auth-methods/index.js @@ -0,0 +1,7 @@ +export default function(visitable, creatable, authMethods, popoverSelect) { + return creatable({ + visit: visitable('/:dc/acls/auth-methods'), + authMethods: authMethods(), + sort: popoverSelect('[data-test-sort-control]'), + }); +} diff --git a/ui/packages/consul-ui/tests/unit/adapters/auth-method-test.js b/ui/packages/consul-ui/tests/unit/adapters/auth-method-test.js new file mode 100644 index 0000000000..9b1e31a6c9 --- /dev/null +++ b/ui/packages/consul-ui/tests/unit/adapters/auth-method-test.js @@ -0,0 +1,12 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Adapter | auth-method', function(hooks) { + setupTest(hooks); + + // Replace this with your real tests. + test('it exists', function(assert) { + let adapter = this.owner.lookup('adapter:auth-method'); + assert.ok(adapter); + }); +}); diff --git a/ui/packages/consul-ui/tests/unit/models/auth-method-test.js b/ui/packages/consul-ui/tests/unit/models/auth-method-test.js new file mode 100644 index 0000000000..ae7107e0b3 --- /dev/null +++ b/ui/packages/consul-ui/tests/unit/models/auth-method-test.js @@ -0,0 +1,14 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; +import { run } from '@ember/runloop'; + +module('Unit | Model | auth-method', function(hooks) { + setupTest(hooks); + + // Replace this with your real tests. + test('it exists', function(assert) { + let store = this.owner.lookup('service:store'); + let model = run(() => store.createRecord('auth-method', {})); + assert.ok(model); + }); +}); diff --git a/ui/packages/consul-ui/tests/unit/serializers/auth-method-test.js b/ui/packages/consul-ui/tests/unit/serializers/auth-method-test.js new file mode 100644 index 0000000000..d1bacfa207 --- /dev/null +++ b/ui/packages/consul-ui/tests/unit/serializers/auth-method-test.js @@ -0,0 +1,23 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Serializer | auth-method', function(hooks) { + setupTest(hooks); + + // Replace this with your real tests. + test('it exists', function(assert) { + let store = this.owner.lookup('service:store'); + let serializer = store.serializerFor('auth-method'); + + assert.ok(serializer); + }); + + test('it serializes records', function(assert) { + let store = this.owner.lookup('service:store'); + let record = store.createRecord('auth-method', {}); + + let serializedRecord = record.serialize(); + + assert.ok(serializedRecord); + }); +}); diff --git a/ui/packages/consul-ui/tests/unit/services/repository/auth-method-test.js b/ui/packages/consul-ui/tests/unit/services/repository/auth-method-test.js new file mode 100644 index 0000000000..5290bfb18f --- /dev/null +++ b/ui/packages/consul-ui/tests/unit/services/repository/auth-method-test.js @@ -0,0 +1,12 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Service | auth-method', function(hooks) { + setupTest(hooks); + + // Replace this with your real tests. + test('it exists', function(assert) { + let service = this.owner.lookup('service:repository/auth-method'); + assert.ok(service); + }); +}); diff --git a/ui/packages/consul-ui/translations/en-us.yaml b/ui/packages/consul-ui/translations/en-us.yaml index db329791f9..21491358c4 100644 --- a/ui/packages/consul-ui/translations/en-us.yaml +++ b/ui/packages/consul-ui/translations/en-us.yaml @@ -6,11 +6,14 @@ common: vault: Vault aws: AWS kubernetes: Kubernetes + jwt: JWT + oidc: OIDC ui: remove: Remove {item} filtered-by: Filtered by {item} name: Name creation: Creation + maxttl: Max TTL consul: name: Name passing: Passing @@ -35,6 +38,7 @@ common: localbindport: Local Bind Port destinationname: Destination Name sourcename: Source Name + displayname: Display Name search: search: Search searchproperty: Search Across @@ -52,6 +56,9 @@ common: age: asc: Oldest to Newest desc: Newest to Oldest + duration: + asc: Longest to shortest + desc: Shortest to longest status: asc: Unhealthy to Healthy desc: Healthy to Unhealthy @@ -127,6 +134,15 @@ components: options: global-management: Global Management standard: Standard + auth-method: + search-bar: + kind: + name: Type + locality: + name: Source + options: + local: Creates local tokens + global: Creates global tokens kv: search-bar: kind: