1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2024-12-10 21:24:20 +01:00

(api) validate auth providers data

+ remove legacy code
This commit is contained in:
Sylvain 2022-04-11 17:27:56 +02:00
parent 04ae91a7d1
commit 70803ee41b
13 changed files with 51 additions and 404 deletions

View File

@ -10,6 +10,7 @@ import { FormSelect } from '../form/form-select';
import { Oauth2Form } from './oauth2-form'; import { Oauth2Form } from './oauth2-form';
import { DataMappingForm } from './data-mapping-form'; import { DataMappingForm } from './data-mapping-form';
import { FabButton } from '../base/fab-button'; import { FabButton } from '../base/fab-button';
import AuthProviderAPI from '../../api/auth-provider';
declare const Application: IApplication; declare const Application: IApplication;
@ -41,11 +42,11 @@ export const ProviderForm: React.FC<ProviderFormProps> = ({ action, provider, on
* Callback triggered when the form is submitted: process with the provider creation or update. * Callback triggered when the form is submitted: process with the provider creation or update.
*/ */
const onSubmit: SubmitHandler<AuthenticationProvider> = (data: AuthenticationProvider) => { const onSubmit: SubmitHandler<AuthenticationProvider> = (data: AuthenticationProvider) => {
if (data) { AuthProviderAPI[action](data).then(() => {
onSuccess('Provider created successfully'); onSuccess(t(`app.shared.authentication.${action}_success`));
} else { }).catch(error => {
onError('Failed to created provider'); onError(error);
} });
}; };
/** /**

View File

@ -46,114 +46,6 @@ const check_oauth2_id_is_mapped = function (mappings) {
return false; return false;
}; };
/**
* Provides a set of common callback methods and data to the $scope parameter. These methods are used
* in the various authentication providers' controllers.
*
* Provides :
* - $scope.authMethods
* - $scope.mappingFields
* - $scope.cancel()
* - $scope.methodName()
* - $scope.defineDataMapping(mapping)
*
* Requires :
* - mappingFieldsPromise: retrieved by AuthProvider.mapping_fields()
* - $state (Ui-Router) [ 'app.admin.members' ]
* - _t : translation method
*/
class AuthenticationController {
constructor ($scope, $state, $uibModal, _t, mappingFieldsPromise) {
// list of supported authentication methods
$scope.authMethods = METHODS;
// list of fields that can be mapped through the SSO
$scope.mappingFields = mappingFieldsPromise;
/**
* Changes the admin's view to the members list page
*/
$scope.cancel = function () { $state.go('app.admin.members'); };
/**
* Return a localized string for the provided method
*/
$scope.methodName = function (method) {
return _t('app.shared.authentication.' + METHODS[method]);
};
/**
* Open a modal allowing to specify the data mapping for the given field
*/
$scope.defineDataMapping = function (mapping) {
$uibModal.open({
templateUrl: '/admin/authentications/_data_mapping.html',
size: 'md',
resolve: {
field () { return mapping; },
datatype () {
for (const field of Array.from($scope.mappingFields[mapping.local_model])) {
if (field[0] === mapping.local_field) {
return field[1];
}
}
}
},
controller: ['$scope', '$uibModalInstance', 'field', 'datatype', function ($scope, $uibModalInstance, field, datatype) {
// parent field
$scope.field = field;
// expected data type
$scope.datatype = datatype;
// data transformation rules
$scope.transformation =
{ rules: field.transformation || { type: datatype } };
// available transformation formats
$scope.formats = {
date: [
{
label: 'ISO 8601',
value: 'iso8601'
},
{
label: 'RFC 2822',
value: 'rfc2822'
},
{
label: 'RFC 3339',
value: 'rfc3339'
},
{
label: 'Timestamp (s)',
value: 'timestamp-s'
},
{
label: 'Timestamp (ms)',
value: 'timestamp-ms'
}
]
};
// Create a new mapping between anything and an expected integer
$scope.addIntegerMapping = function () {
if (!angular.isArray($scope.transformation.rules.mapping)) {
$scope.transformation.rules.mapping = [];
}
return $scope.transformation.rules.mapping.push({ from: '', to: 0 });
};
// close and save the modifications
$scope.ok = function () { $uibModalInstance.close($scope.transformation.rules); };
// do not save the modifications
$scope.cancel = function () { $uibModalInstance.dismiss(); };
}]
})
.result.finally(null).then(function (transfo_rules) { mapping.transformation = transfo_rules; });
};
}
}
/** /**
* Page listing all authentication providers * Page listing all authentication providers
*/ */
@ -323,9 +215,6 @@ Application.Controllers.controller('NewAuthenticationController', ['$scope', '$s
} }
} }
}; };
// Using the AuthenticationController
return new AuthenticationController($scope, $state, $uibModal, _t, mappingFieldsPromise);
} }
]); ]);
@ -359,7 +248,18 @@ Application.Controllers.controller('EditAuthenticationController', ['$scope', '$
); );
}; };
// Using the AuthenticationController /**
return new AuthenticationController($scope, $state, $uibModal, _t, mappingFieldsPromise); * Shows a success message forwarded from a child react component
*/
$scope.onSuccess = function (message) {
growl.success(message);
};
/**
* Callback triggered by react components
*/
$scope.onError = function (message) {
growl.error(message);
};
} }
]); ]);

View File

@ -3,7 +3,7 @@ import { Control } from 'react-hook-form/dist/types/form';
export type ruleTypes<TFieldValues> = { export type ruleTypes<TFieldValues> = {
required?: boolean | string, required?: boolean | string,
pattern?: RegExp | {value: RegExp, message: string}, pattern?: RegExp | { value: RegExp, message: string },
minLength?: number, minLength?: number,
maxLength?: number, maxLength?: number,
min?: number, min?: number,

View File

@ -1,65 +0,0 @@
<div class="modal-header">
<h3 class="modal-title"><span translate>{{ 'app.shared.authentication.data_mapping' }}</span> : {{field.local_field}}</h3>
</div>
<div class="modal-body m-lg">
<div>
<span translate>{{ 'app.shared.authentication.expected_data_type' }}</span> : {{datatype}}
</div>
<form name="mappingForm" class="m-t-md">
<ng-switch on="datatype">
<!-- BOOLEAN -->
<div ng-switch-when="boolean">
<label for="add_mapping" translate>{{ 'app.shared.authentication.mappings' }}</label>
<ul class="list-unstyled">
<li class="m-t-sm m-l">
<input type="text"
name="true_value"
id="true_value"
class="form-control inline width-35 m-r "
ng-model="transformation.rules.false_value">
<i class="fa fa-arrows-h"></i>
<label for="true_value" class="m-l">true</label>
</li>
<li class="m-t-sm m-l">
<input type="text"
name="false_value"
id="false_value"
class="form-control inline width-35 m-r "
ng-model="transformation.rules.true_value">
<i class="fa fa-arrows-h"></i>
<label for="false_value" class="m-l">false</label>
</li>
</ul>
</div>
<!-- DATE -->
<div ng-switch-when="date">
<label for="date_format" translate>{{ 'app.shared.authentication.input_format' }}</label>
<select name="date_format"
id="date_format"
class="form-control"
ng-model="transformation.rules.format"
ng-options="format.value as format.label for format in formats.date">
</select>
</div>
<!-- INTEGER -->
<div ng-switch-when="integer">
<label for="add_mapping" translate>{{ 'app.shared.authentication.mappings' }}</label>
<button class="btn btn-default pull-right" ng-click="addIntegerMapping()"><i class="fa fa-plus"></i></button>
<ul class="list-unstyled">
<li ng-repeat="map in transformation.rules.mapping" class="m-t-sm m-l">
<input type="text" class="form-control inline width-35 m-r " ng-model="map.from">
<i class="fa fa-arrows-h"></i>
<input type="number" class="form-control inline width-35 m-l" ng-model="map.to">
</li>
</ul>
</div>
</ng-switch>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-primary" ng-click="ok()" ng-disabled="!mappingForm.$valid" ng-if="datatype != 'string' && datatype != 'text'" translate>{{ 'app.shared.buttons.confirm' }}</button>
<button class="btn btn-warning" ng-click="cancel()" translate>{{ 'app.shared.buttons.cancel' }}</button>
</div>

View File

@ -1,30 +0,0 @@
<div class="form-group" ng-class="{'has-error': providerForm['auth_provider[name]'].$dirty && providerForm['auth_provider[name]'].$invalid}">
<label for="provider_name" class="col-sm-3 control-label" translate>{{ 'app.shared.authentication.name' }}</label>
<div class="col-sm-9">
<input type="text"
ng-model="provider.name"
class="form-control"
name="auth_provider[name]"
id="provider_name"
ng-disabled="mode == 'edition'"
required />
<span class="help-block" ng-show="providerForm['auth_provider[name]'].$dirty && providerForm['auth_provider[name]'].$error.required" translate>{{ 'app.shared.authentication.provider_name_is_required' }}</span>
</div>
</div>
<div class="form-group" ng-class="{'has-error': providerForm['auth_provider[providable_type]'].$dirty && providerForm['auth_provider[providable_type]'].$invalid}">
<label for="provider_type" class="col-sm-3 control-label" translate>{{ 'app.shared.authentication.authentication_type' }}</label>
<div class="col-sm-9">
<select ng-model="provider.providable_type"
ng-change="updateProvidable()"
class="form-control"
name="auth_provider[providable_type]"
id="provider_type"
ng-options="key as methodName(key) for (key, value) in authMethods"
ng-disabled="mode == 'edition'"
required>
</select>
<input type="hidden" name="auth_provider[type]" ng-value="provider.type" />
<span class="help-block" ng-show="providerForm['auth_provider[providable_type]'].$dirty && providerForm['auth_provider[providable_type]'].$error.required" translate>{{ 'app.shared.authentication.authentication_type_is_required' }}</span>
</div>
</div>

View File

@ -1,104 +0,0 @@
<hr/>
<div class="form-group" ng-class="{'has-error': providerForm['auth_provider[base_url]'].$dirty && providerForm['auth_provider[base_url]'].$invalid}">
<label for="provider_base_url" class="col-sm-3 control-label" translate>{{ 'app.shared.oauth2.common_url' }}</label>
<div class="col-sm-9">
<input type="text"
ng-model="provider.providable_attributes.base_url"
class="form-control"
name="auth_provider[base_url]"
id="provider_base_url"
placeholder="https://sso.example.net..."
required
url>
<span class="help-block" ng-show="providerForm['auth_provider[base_url]'].$dirty && providerForm['auth_provider[base_url]'].$error.required" translate>{{ 'app.shared.oauth2.common_url_is_required' }}</span>
<span class="help-block" ng-show="providerForm['auth_provider[base_url]'].$error.url" translate>{{ 'app.shared.oauth2.provided_url_is_not_a_valid_url' }}</span>
</div>
</div>
<div class="form-group" ng-class="{'has-error': providerForm['auth_provider[authorization_endpoint]'].$dirty && providerForm['auth_provider[authorization_endpoint]'].$invalid}">
<label for="provider_authorization_endpoint" class="col-sm-3 control-label" translate>{{ 'app.shared.oauth2.authorization_endpoint' }}</label>
<div class="col-sm-9">
<input type="text"
ng-model="provider.providable_attributes.authorization_endpoint"
class="form-control"
name="auth_provider[authorization_endpoint]"
id="provider_authorization_endpoint"
placeholder="/oauth2/auth..."
required
endpoint>
<span class="help-block" ng-show="providerForm['auth_provider[authorization_endpoint]'].$dirty && providerForm['auth_provider[authorization_url]'].$error.required" translate>{{ 'app.shared.oauth2.oauth2_authorization_endpoint_is_required' }}</span>
<span class="help-block" ng-show="providerForm['auth_provider[authorization_endpoint]'].$error.endpoint" translate>{{ 'app.shared.oauth2.provided_endpoint_is_not_valid' }}</span>
</div>
</div>
<div class="form-group" ng-class="{'has-error': providerForm['auth_provider[token_endpoint]'].$dirty && providerForm['auth_provider[token_endpoint]'].$invalid}">
<label for="provider_token_endpoint" class="col-sm-3 control-label" translate>{{ 'app.shared.oauth2.token_acquisition_endpoint' }}</label>
<div class="col-sm-9">
<input type="text"
ng-model="provider.providable_attributes.token_endpoint"
class="form-control"
name="auth_provider[token_endpoint]"
id="provider_token_endpoint"
placeholder="/oauth2/token..."
required
endpoint>
<span class="help-block" ng-show="providerForm['auth_provider[token_endpoint]'].$dirty && providerForm['auth_provider[token_endpoint]'].$error.required" translate>{{ 'app.shared.oauth2.oauth2_token_acquisition_endpoint_is_required' }}</span>
<span class="help-block" ng-show="providerForm['auth_provider[token_endpoint]'].$error.endpoint" translate>{{ 'app.shared.oauth2.provided_endpoint_is_not_valid' }}</span>
</div>
</div>
<div class="form-group" ng-class="{'has-error': providerForm['auth_provider[profile_url]'].$dirty && providerForm['auth_provider[profile_url]'].$invalid}">
<label for="provider_profile_url" class="col-sm-3 control-label" translate>{{ 'app.shared.oauth2.profil_edition_url' }}</label>
<div class="col-sm-9">
<input type="text"
ng-model="provider.providable_attributes.profile_url"
class="form-control"
name="auth_provider[profile_url]"
id="provider_profile_url"
placeholder="https://exemple.net/user..."
required
url>
<span class="help-block" ng-show="providerForm['auth_provider[profile_url]'].$dirty && providerForm['auth_provider[profile_url]'].$error.required" translate>{{ 'app.shared.oauth2.profile_edition_url_is_required' }}</span>
<span class="help-block" ng-show="providerForm['auth_provider[profile_url]'].$error.url" translate>{{ 'app.shared.oauth2.provided_url_is_not_a_valid_url' }}</span>
</div>
</div>
<div class="form-group" ng-class="{'has-error': providerForm['auth_provider[client_id]'].$dirty && providerForm['auth_provider[client_id]'].$invalid}">
<label for="provider_client_id" class="col-sm-3 control-label" translate>{{ 'app.shared.oauth2.client_identifier' }}</label>
<div class="col-sm-9">
<input type="text"
ng-model="provider.providable_attributes.client_id"
class="form-control"
name="auth_provider[client_id]"
id="provider_client_id"
required>
<span class="help-block" ng-show="providerForm['auth_provider[client_id]'].$dirty && providerForm['auth_provider[client_id]'].$error.required" translate>{{ 'app.shared.oauth2.oauth2_client_identifier_is_required' }} {{ 'obtain_it_when_registering_with_your_provider' }}</span>
</div>
</div>
<div class="form-group" ng-class="{'has-error': providerForm['auth_provider[client_secret]'].$dirty && providerForm['auth_provider[client_secret]'].$invalid}">
<label for="provider_client_secret" class="col-sm-3 control-label" translate>{{ 'app.shared.oauth2.client_secret' }}</label>
<div class="col-sm-9">
<input type="text"
ng-model="provider.providable_attributes.client_secret"
class="form-control"
name="auth_provider[client_secret]"
id="provider_client_secret"
required>
<span class="help-block" ng-show="providerForm['auth_provider[client_secret]'].$dirty && providerForm['auth_provider[client_secret]'].$error.required" translate>{{ 'app.shared.oauth2.oauth2_client_secret_is_required' }} {{ 'obtain_it_when_registering_with_your_provider' }}</span>
</div>
</div>
<div class="form-group" ng-class="{'has-error': providerForm['auth_provider[scopes]'].$dirty && providerForm['auth_provider[scopes]'].$invalid}">
<label for="provider_client_secret" class="col-sm-3 control-label" translate>{{ 'app.shared.oauth2.scopes' }}</label>
<div class="col-sm-9">
<input type="text"
ng-model="provider.providable_attributes.scopes"
class="form-control"
name="auth_provider[scopes]"
id="provider_scopes"
placeholder="profile,email...">
</div>
</div>
<ng-include src="'/admin/authentications/_oauth2_mapping.html'"></ng-include>

View File

@ -1,80 +0,0 @@
<h4 class="m-l m-t-xl" translate>{{ 'app.shared.oauth2.define_the_fields_mapping' }}</h4>
<button type="button" class="btn btn-success m-l m-b" ng-click="newMapping = {}"><i class="fa fa-plus"></i> {{ 'app.shared.oauth2.add_a_match' | translate }}</button>
<table class="table">
<thead>
<tr>
<th translate>{{ 'app.shared.oauth2.model' }}</th>
<th translate>{{ 'app.shared.oauth2.field' }}</th>
<th translate>{{ 'app.shared.oauth2.api_endpoint_url' }}</th>
<th translate>{{ 'app.shared.oauth2.api_type' }}</th>
<th translate>{{ 'app.shared.oauth2.api_fields' }}</th>
<th style="width: 6.4em;"></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="m in provider.providable_attributes.auth_provider_mappings_attributes" ng-if="!m._destroy">
<td class="text-c">{{m.local_model}}</td>
<td>{{m.local_field}}</td>
<td>{{m.api_endpoint}}</td>
<td>{{m.api_data_type}}</td>
<td>{{m.api_field}}</td>
<td>
<button class="btn btn-info" ng-click="defineDataMapping(m)">
<i class="fa fa-info-circle"></i>
</button>
<button class="btn btn-danger" ng-click="m._destroy = true">
<i class="fa fa-trash-o"></i>
</button>
</td>
</tr>
<tr ng-show="newMapping" ng-form="mappingForm" isolate-form>
<td ng-class="{'has-error': mappingForm['auth_mapping[local_model]'].$dirty && mappingForm['auth_mapping[local_model]'].$invalid}">
<select class="form-control text-c"
name="auth_mapping[local_model]"
ng-options="model as model for (model, fields) in mappingFields"
ng-model="newMapping.local_model"
required>
</select>
</td>
<td ng-class="{'has-error': mappingForm['auth_mapping[local_field]'].$dirty && mappingForm['auth_mapping[local_field]'].$invalid}">
<select class="form-control"
name="auth_mapping[local_field]"
ng-options="field[0] as field[0] for field in mappingFields[newMapping.local_model]"
ng-model="newMapping.local_field"
required>
</select>
</td>
<td ng-class="{'has-error': mappingForm['auth_mapping[api_endpoint]'].$dirty && mappingForm['auth_mapping[api_endpoint]'].$invalid}">
<input type="text"
class="form-control"
placeholder="/api/resource..."
ng-model="newMapping.api_endpoint"
name="auth_mapping[api_endpoint]"
required/>
</td>
<td ng-class="{'has-error': mappingForm['auth_mapping[api_data_type]'].$dirty && mappingForm['auth_mapping[api_data_type]'].$invalid}">
<select class="form-control"
ng-model="newMapping.api_data_type"
name="auth_mapping[api_data_type]"
required>
<option value="json">JSON</option>
</select>
</td>
<td ng-class="{'has-error': mappingForm['auth_mapping[api_field]'].$dirty && mappingForm['auth_mapping[api_field]'].$invalid}">
<input type="text"
class="form-control help-cursor"
placeholder="field_name"
ng-model="newMapping.api_field"
name="auth_mapping[api_field]"
title="{{ 'app.shared.oauth2.api_field_help' | translate }}"
required/>
</td>
<td>
<button type="button" class="btn btn-success" ng-disabled="mappingForm.$invalid" ng-click="provider.auth_provider_mappings_attributes.push(newMapping); newMapping = null;"><i class="fa fa-check"></i></button>
<button type="button" class="btn btn-danger" ng-click="newMapping = null"><i class="fa fa-times"></i></button>
</td>
</tr>
</tbody>
</table>

View File

@ -35,14 +35,10 @@
<section class="panel panel-default bg-light m-lg"> <section class="panel panel-default bg-light m-lg">
<div class="panel-body m-r"> <div class="panel-body m-r">
<ng-include src="'/admin/authentications/_form.html'"></ng-include>
<ng-include src="'/admin/authentications/_oauth2.html'" ng-if="provider.providable_type == 'OAuth2Provider'"></ng-include>
</div> <!-- ./panel-body -->
<provider-form action="'update'" on-success="onSuccess" on-error="onError" provider="provider"></provider-form>
<div class="panel-footer no-padder">
<input type="button" value="{{ 'app.shared.buttons.confirm_changes' | translate }}" class="r-b btn-valid btn btn-warning btn-block p-lg btn-lg text-u-c" ng-disabled="providerForm.$invalid" ng-click="updateProvider()"/>
</div> </div>
</section> </section>
</form> </form>

View File

@ -21,6 +21,9 @@ class AuthProvider < ApplicationRecord
has_many :auth_provider_mappings, dependent: :destroy has_many :auth_provider_mappings, dependent: :destroy
accepts_nested_attributes_for :auth_provider_mappings, allow_destroy: true accepts_nested_attributes_for :auth_provider_mappings, allow_destroy: true
validates :providable_type, inclusion: { in: PROVIDABLE_TYPES }
validates :name, presence: true, uniqueness: true
before_create :set_initial_state before_create :set_initial_state
def build_providable(params) def build_providable(params)

View File

@ -5,6 +5,8 @@
class DatabaseProvider < ApplicationRecord class DatabaseProvider < ApplicationRecord
has_one :auth_provider, as: :providable, dependent: :destroy has_one :auth_provider, as: :providable, dependent: :destroy
validates_with DatabaseProviderValidator
def profile_url def profile_url
'/#!/dashboard/profile' '/#!/dashboard/profile'
end end

View File

@ -5,6 +5,8 @@
class OAuth2Provider < ApplicationRecord class OAuth2Provider < ApplicationRecord
has_one :auth_provider, as: :providable has_one :auth_provider, as: :providable
validates_with OAuth2ProviderValidator
def domain def domain
URI(base_url).scheme + '://' + URI(base_url).host URI(base_url).scheme + '://' + URI(base_url).host
end end

View File

@ -0,0 +1,10 @@
# frozen_string_literal: true
# Validates there's only one database provider
class DatabaseProviderValidator < ActiveModel::Validator
def validate(record)
return if DatabaseProvider.count.zero?
record.errors[:id] << I18n.t('app.admin.authentication_new.a_local_database_provider_already_exists_unable_to_create_another')
end
end

View File

@ -0,0 +1,12 @@
# frozen_string_literal: true
# Validates the presence of the User.uid mapping
class OAuth2ProviderValidator < ActiveModel::Validator
def validate(record)
return if record.auth_provider.auth_provider_mappings.any? do |mapping|
mapping.local_model == 'user' && mapping.local_field == 'uid'
end
record.errors.add(:uid, I18n.t('app.admin.authentication_new.it_is_required_to_set_the_matching_between_User.uid_and_the_API_to_add_this_provider'))
end
end