/** * Copyright (c) HashiCorp, Inc. * SPDX-License-Identifier: BUSL-1.1 */ import { inject as service } from '@ember/service'; import RepositoryService from 'consul-ui/services/repository'; import { getOwner } from '@ember/application'; import { set } from '@ember/object'; import dataSource from 'consul-ui/decorators/data-source'; const modelName = 'oidc-provider'; const OAUTH_PROVIDER_NAME = 'oidc-with-url'; export default class OidcProviderService extends RepositoryService { @service('torii') manager; @service('settings') settings; init() { super.init(...arguments); this.provider = getOwner(this).lookup(`torii-provider:${OAUTH_PROVIDER_NAME}`); } getModelName() { return modelName; } @dataSource('/:partition/:ns/:dc/oidc/providers') async findAllByDatacenter() { const res = await super.findAllByDatacenter(...arguments); if (res.length === 0) { const err = new Error('Not found'); err.statusCode = 404; this.store.adapterFor(this.getModelName()).error(err); return; } return res; } @dataSource('/:partition/:ns/:dc/oidc/provider/:id') async findBySlug(params) { // This addition is mainly due to ember-data book-keeping This is one of // the only places where Consul w/namespaces enabled doesn't return a // response with a Namespace property, but in order to keep ember-data // id's happy we need to fake one. Usually when we make a request to consul // with an empty `ns=` Consul will use the namespace that is assigned to // the token, and when we get the response we can pick that back off the // responses `Namespace` property. As we don't receive a `Namespace` // property here, we have to figure this out ourselves. But we also want // to make this completely invisible to 'the application engineer/a // template engineer'. This feels like the best place/way to do it as we // are already in a asynchronous method, and we avoid adding extra 'just // for us' parameters to the query object. There is a chance that as we // are discovering the tokens default namespace on the frontend and // assigning that to the ns query param, the token default namespace 'may' // have changed by the time the request hits the backend. As this is // extremely unlikely and in the scheme of things not a big deal, we // decided that doing this here is ok and avoids doing this in a more // complicated manner. const token = (await this.settings.findBySlug('token')) || {}; return super.findBySlug({ ns: params.ns || token.Namespace || 'default', partition: params.partition || token.Partition || 'default', dc: params.dc, id: params.id, }); } @dataSource('/:partition/:ns/:dc/oidc/authorize/:id/:code/:state') authorize(params, configuration = {}) { return this.store.authorize(this.getModelName(), params); } logout(id, code, state, dc, nspace, configuration = {}) { // TODO: Temporarily call this secret, as we alreayd do that with // self in the `store` look to see whether we should just call it id like // the rest const query = { id: id, }; return this.store.logout(this.getModelName(), query); } close() { this.manager.close(OAUTH_PROVIDER_NAME); } findCodeByURL(src) { // TODO: Maybe move this to the provider itself set(this.provider, 'baseUrl', src); return this.manager.open(OAUTH_PROVIDER_NAME, {}).catch((e) => { let err; switch (true) { case e.message.startsWith('remote was closed'): err = new Error('Remote was closed'); err.statusCode = 499; break; default: err = new Error(e.message); err.statusCode = 500; } this.store.adapterFor(this.getModelName()).error(err); }); } }