1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2024-11-28 09:24:24 +01:00

(ui) display authorization callback url directly in interface

This commit is contained in:
Sylvain 2022-04-12 15:20:10 +02:00
parent 63b03568e4
commit f9e5e7f2a8
14 changed files with 132 additions and 27 deletions

View File

@ -28,6 +28,12 @@ class API::AuthProvidersController < API::ApiController
end
end
def strategy_name
authorize AuthProvider
@provider = AuthProvider.new(providable_type: params[:providable_type], name: params[:name])
render json: @provider.strategy_name
end
def show
authorize AuthProvider
end

View File

@ -31,4 +31,9 @@ export default class AuthProviderAPI {
const res: AxiosResponse<MappingFields> = await apiClient.get('/api/auth_providers/mapping_fields');
return res?.data;
}
static async strategyName (authProvider: AuthenticationProvider): Promise<string> {
const res: AxiosResponse<string> = await apiClient.get(`/api/auth_providers/strategy_name?providable_type=${authProvider.providable_type}&name=${authProvider.name}`);
return res?.data;
}
}

View File

@ -12,5 +12,6 @@ These components must be written using the following conventions:
- All methods in the component must be commented with a comment block.
- Other constants and variables must be commented with an inline block.
- Depending on if we want to use the `<Suspense>` wrapper or not, we can export the component directly or wrap it in a `<Loader>` wrapper.
- When a component is used in angularJS, the wrapped is required. The component must be named like `const Foo` (no export if not used in React) and must have a `const FooWrapper` at the end of its file, which wraps the component in a `<Loader>`.
- When a component is used in angularJS, the wrapper is required. The component must be named like `const Foo` (no export if not used in React) and must have a `const FooWrapper` at the end of its file, which wraps the component in a `<Loader>`.
- Translations must be grouped per component. For example, the `FooBar` component must have its translations in the `config/locales/app.$SCOPE.en.yml` file, under the `foo_bar` key.

View File

@ -3,15 +3,17 @@ import { FormInput } from '../form/form-input';
import { UseFormRegister } from 'react-hook-form';
import { FieldValues } from 'react-hook-form/dist/types/fields';
import { useTranslation } from 'react-i18next';
import { FabOutputCopy } from '../base/fab-output-copy';
interface Oauth2FormProps<TFieldValues> {
register: UseFormRegister<TFieldValues>,
callbackUrl?: string,
}
/**
* Partial form to fill the OAuth2 settings for a new/existing authentication provider.
*/
export const Oauth2Form = <TFieldValues extends FieldValues>({ register }: Oauth2FormProps<TFieldValues>) => {
export const Oauth2Form = <TFieldValues extends FieldValues>({ register, callbackUrl }: Oauth2FormProps<TFieldValues>) => {
const { t } = useTranslation('admin');
// regular expression to validate the the input fields
@ -21,6 +23,7 @@ export const Oauth2Form = <TFieldValues extends FieldValues>({ register }: Oauth
return (
<div className="oauth2-form">
<hr/>
<FabOutputCopy text={callbackUrl} label={t('app.admin.authentication.oauth2_form.authorization_callback_url')} />
<FormInput id="providable_attributes.base_url"
register={register}
placeholder="https://sso.example.net..."

View File

@ -1,6 +1,7 @@
import React, { useState } from 'react';
import { useForm, SubmitHandler } from 'react-hook-form';
import React, { useCallback, useEffect, useState } from 'react';
import { useForm, SubmitHandler, useWatch } from 'react-hook-form';
import { react2angular } from 'react2angular';
import { debounce as _debounce } from 'lodash';
import { AuthenticationProvider } from '../../models/authentication-provider';
import { Loader } from '../base/loader';
import { IApplication } from '../../models/application';
@ -17,7 +18,7 @@ declare const Application: IApplication;
// list of supported authentication methods
const METHODS = {
DatabaseProvider: 'local_database',
OAuth2Provider: 'o_auth2',
OAuth2Provider: 'oauth2',
OpenIdConnectProvider: 'openid_connect'
};
@ -35,9 +36,16 @@ type selectProvidableTypeOption = { value: string, label: string };
*/
export const ProviderForm: React.FC<ProviderFormProps> = ({ action, provider, onError, onSuccess }) => {
const { handleSubmit, register, control } = useForm<AuthenticationProvider>({ defaultValues: { ...provider } });
const output = useWatch({ control });
const [providableType, setProvidableType] = useState<string>(provider?.providable_type);
const [strategyName, setStrategyName] = useState<string>(provider?.strategy_name);
const { t } = useTranslation('admin');
useEffect(() => {
updateStrategyName(output as AuthenticationProvider);
}, [output?.providable_type, output?.name]);
/**
* Callback triggered when the form is submitted: process with the provider creation or update.
*/
@ -66,6 +74,24 @@ export const ProviderForm: React.FC<ProviderFormProps> = ({ action, provider, on
setProvidableType(type);
};
/**
* Request the API the strategy name for the current "in-progress" provider.
*/
const updateStrategyName = useCallback(_debounce((provider: AuthenticationProvider): void => {
AuthProviderAPI.strategyName(provider).then(strategyName => {
setStrategyName(strategyName);
}).catch(error => {
onError(error);
});
}, 400), []);
/**
* Build the callback URL, based on the strategy name.
*/
const buildCallbackUrl = (): string => {
return `${window.location.origin}/users/auth/${strategyName}/callback`;
};
return (
<form className="provider-form" onSubmit={handleSubmit(onSubmit)}>
<FormInput id="name"
@ -80,7 +106,7 @@ export const ProviderForm: React.FC<ProviderFormProps> = ({ action, provider, on
onChange={onProvidableTypeChange}
readOnly={action === 'update'}
rules={{ required: true }} />
{providableType === 'OAuth2Provider' && <Oauth2Form register={register} />}
{providableType === 'OAuth2Provider' && <Oauth2Form register={register} callbackUrl={buildCallbackUrl()} />}
{providableType && providableType !== 'DatabaseProvider' && <DataMappingForm register={register} control={control} />}
<div className="main-actions">
<FabButton type="submit" className="submit-button">{t('app.admin.authentication.provider_form.save')}</FabButton>

View File

@ -0,0 +1,41 @@
import React from 'react';
interface FabOutputCopyProps {
text: string,
onCopy?: () => void,
label?: string,
}
/**
* This component shows a read-only input text filled with the provided text. A button allows to copy the text to the clipboard.
*/
export const FabOutputCopy: React.FC<FabOutputCopyProps> = ({ label, text, onCopy }) => {
const [copied, setCopied] = React.useState(false);
/**
* Copy the given text to the clipboard.
*/
const textToClipboard = () => {
if (navigator && navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text);
if (typeof onCopy === 'function') onCopy();
setCopied(true);
setTimeout(() => setCopied(false), 1000);
}
};
return (
<div className="fab-output-copy">
<label className="form-item">
<div className='form-item-header'>
<p>{label}</p>
</div>
<div className='form-item-field'>
<input value={text} readOnly />
<span className="addon">
<button className={copied ? 'copied' : ''} onClick={textToClipboard}><i className="fa fa-clipboard" /></button>
</span>
</div>
</label>
</div>
);
};

View File

@ -23,6 +23,7 @@
@import "modules/base/fab-button";
@import "modules/base/fab-input";
@import "modules/base/fab-modal";
@import "modules/base/fab-output-copy";
@import "modules/base/fab-popover";
@import "modules/base/fab-text-editor";
@import "modules/base/labelled-input";

View File

@ -0,0 +1,22 @@
.fab-output-copy {
.form-item-field {
& > input {
background-color: var(--gray-soft);
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
& > .addon > button.copied {
position: relative;
&:after {
content: '\f00c';
font-family: 'Font Awesome 5 Free';
position: absolute;
right: 0;
width: 100%;
background-color: #EFEFEF;
}
}
}
}

View File

@ -76,7 +76,7 @@ class AuthProvider < ApplicationRecord
## Return the provider type name without the "Provider" part.
## eg. DatabaseProvider will return 'database'
def provider_type
providable.class.name[0..-9].downcase
providable_type[0..-9].downcase
end
## Return the user's profile fields that are currently managed from the SSO

View File

@ -1,3 +1,6 @@
# frozen_string_literal: true
# Check the access policies for API::AuthProvidersController
class AuthProviderPolicy < ApplicationPolicy
class Scope < Scope
@ -6,7 +9,7 @@ class AuthProviderPolicy < ApplicationPolicy
end
end
%w(index? show? create? update? destroy? mapping_fields?).each do |action|
%w[index? show? create? update? destroy? mapping_fields? strategy_name?].each do |action|
define_method action do
user.admin?
end

View File

@ -41,7 +41,6 @@
Fablab.defaultHost = "<%= Rails.application.secrets.default_host %>";
Fablab.trackingId = "<%= Setting.get('tracking_id') %>";
Fablab.adminSysId = parseInt("<%= User.adminsys&.id %>", 10);
Fablab.baseHostUrl = "<%= Rails.application.secrets.default_host %>";
// i18n stuff
Fablab.locale = "<%= Rails.application.secrets.app_locale %>";

View File

@ -1089,6 +1089,7 @@ en:
date: "date"
boolean: "boolean"
oauth2_form:
authorization_callback_url: "Authorization callback URL"
common_url: "Server root URL"
authorization_endpoint: "Authorization endpoint"
token_acquisition_endpoint: "Token acquisition endpoint"
@ -1102,7 +1103,7 @@ en:
save: "Save"
methods:
local_database: "Local database"
o_auth2: "OAuth 2.0"
oauth2: "OAuth 2.0"
openid_connect: "OpenID Connect"
#create a new authentication provider (SSO)
authentication_new:

View File

@ -151,6 +151,7 @@ Rails.application.routes.draw do
get 'mapping_fields', on: :collection
get 'active', action: 'active', on: :collection
post 'send_code', action: 'send_code', on: :collection
get 'strategy_name', action: 'strategy_name', on: :collection
end
resources :abuses, only: %i[index create destroy]
resources :open_api_clients, only: %i[index create update destroy] do

View File

@ -5,27 +5,23 @@ For this guide, we will use [GitHub](https://developer.github.com/v3/oauth/) as
- First, you must have a GitHub account. This is free, so create one if you don't have any.
Visit https://github.com/join?source=login to create an account.
- Secondly, you will need to register your Fab-manager instance as an application in GitHub.
Visit https://github.com/settings/applications/new to register your instance.
- In `Application name`, we advise you to set the same name as your Fab-manager's instance title.
- In `Homepage URL`, put the public URL where your Fab-manager's instance is located (eg. https://example.com).
- In `Authorization callback URL`, you must specify an URL that will match this scheme: https://example.com/users/auth/oauth2-github/callback
- **example.com** is your own Fab-manager's address
- **oauth2-github** match the provider's "strategy name" in the Fab-manager.
It is composed of: **SSO's protocol**, _dash_, **slug of the provider's name**.
If you have a doubt about what it will be, set any value, create the authentication provider in your Fab-manager (see below), then the strategy's name will be shown in the providers list.
Afterwards, edit your app on GitHub to set the correct name.
- You'll be redirected to a page displaying two important information: your **Client ID** and your **Client Secret**.
- Now go to your Fab-manager's instance, login as an administrator, go to `Users management` and `Authentication`.
- Go to your Fab-manager's instance, login as an administrator, go to `Users management` and `Authentication`.
Click `Add a new authentication provider`, and select _OAuth 2.0_ in the `Authentication type` drop-down list.
In `name`, you can set whatever you want, but you must be aware that:
1. You will need to type this name in a terminal to activate the provider, so prefer avoiding chars that must be escaped.
2. This name will be occasionally displayed to end users, so prefer sweet and speaking names.
3. The slug of this name is used in the callback URL provided to the SSO server (eg. /users/auth/oauth2-**github**/callback)
- Fulfill the form with the following parameters:
- You'll see an "Authorization Callback URL" field, generated based on what you typed previously. Copy the content of this field to your clipboard.
- Now, you will need to register your Fab-manager instance as an application in GitHub.
Visit https://github.com/settings/applications/new to register your instance.
- In `Application name`, we advise you to set the same name as your Fab-manager's instance title.
- In `Homepage URL`, put the public URL where your Fab-manager's instance is located (eg. https://example.com).
- In `Authorization callback URL`, you must paste the URL previously copied from Fa-manager.
- You'll be redirected to a page displaying two important information: your **Client ID** and your **Client Secret**. Keep them safe, you'll need them to configure Fab-manager.
- Now go back to your Fab-manager's configuration interface and fulfill the remaining form with the following parameters:
- **Server root URL**: `https://github.com` This is the domain name of the where the SSO server is located.
- **Authorization endpoint**: `/login/oauth/authorize` This URL can be found [here](https://developer.github.com/v3/oauth/).
- **Token Acquisition Endpoint**: `/login/oauth/access_token` This URL can be found [here](https://developer.github.com/v3/oauth/).
@ -33,7 +29,7 @@ For this guide, we will use [GitHub](https://developer.github.com/v3/oauth/) as
- **Client identifier**: Your Client ID, collected just before.
- **Client secret**: Your Client Secret, collected just before.
Please note that in some cases we'll encounter an issue unless the **common URL** must only contain the root domain (e.g. `http://github.com`), and the other parts of the URL must go to **Authorization endpoint** (e.g. `/login/oauth/authorize`) and **Token Acquisition Endpoint** (e.g. `/login/oauth/access_token`).
Please note the **common URL** must only contain the root domain (e.g. `http://github.com`), and the other parts of the URL must go to **Authorization endpoint** (e.g. `/login/oauth/authorize`) and **Token Acquisition Endpoint** (e.g. `/login/oauth/access_token`).
- Then you will need to define the matching of the fields between the Fab-manager and what the external SSO can provide.
Please note that the only mandatory field is `User.uid`.