Skip to content

Commit

Permalink
Merge pull request #370 from TeskaLabs/feature/implement-tenant-sessions
Browse files Browse the repository at this point in the history
Implement tenant sessions
  • Loading branch information
Pe5h4 authored Dec 1, 2022
2 parents 343d91c + 7fdb49e commit e7c9b7e
Show file tree
Hide file tree
Showing 14 changed files with 184 additions and 155 deletions.
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

## Release Candidate

## v22.48

### Breaking changes

- Access to tenants must be requested in authorization scope. This is related to tenant session implementation in ASAB WebUI [here](https://github.com/TeskaLabs/asab-webui/pull/370) (PLUM Sprint 221118, [!92](https://github.com/TeskaLabs/seacat-auth/pull/92))

### Features

- Implement tenant sessions, implement access denied handling, implement access denied card, remove tenant selector card (INDIGO Sprint 221125, [!370](https://github.com/TeskaLabs/asab-webui/pull/370))

### Refactoring

- Refactor Configuration module scroll styling (INDIGO Sprint 221111, [!378](https://github.com/TeskaLabs/asab-webui/pull/378))
Expand All @@ -10,7 +20,6 @@

- Fix styles for so long title in CardHeader (INDIGO Sprint 221125, [!377](https://github.com/TeskaLabs/asab-webui/pull/377))


## v22.46

### Features
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ module.exports = {
MOCK_USERINFO: {
"email": "[email protected]",
"phone_number": "0123456789",
"preferred_username": "Test",
"username": "Test",
"resources": ["test:testy:read"],
"roles": ["default/Gringo"],
"sub": "tst:123456789",
Expand Down
12 changes: 6 additions & 6 deletions demo/public/locales/cs/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@
"Resources": "Zdroje",
"Previous screen": "Zpět na předchozí okno"
},
"AccessDeniedCard": {
"Access denied": "Přístup zamítnut",
"Please contact application administrator for assigning appropriate access rights.": "Kontaktujte prosím administrátora aplikace se žádostí o přidělení přístupu.",
"Silly as it sounds, redirection back to login failed": "Nestává se to často, ale přesměrování zpět na přihlášení selhalo",
"Leave": "Odejít"
},
"AuthHeaderDropdown": {
"My account": "Můj účet",
"Access control": "Přístupová oprávnění",
Expand Down Expand Up @@ -145,12 +151,6 @@
"Version": "Verze",
"Repository": "Repozitář"
},
"TenantSelectionCard": {
"Select valid tenant to enter the application": "Vyberte platný tenant pro vstup do aplikace",
"Silly as it sounds, redirection back to login failed": "Nestává se to často, ale přesměrování zpět na přihlášení selhalo",
"Select tenant": "Vyberte tenanta",
"Back to login": "Zpět na přihlášení"
},
"Showing item(s)": "{{ from }} - {{ to }} z {{ total }} položek",
"You do not have access rights to perform this action": "Váš přístup je omezen, nemáte dostatečná oprávnění",
"Items per page": "Položek na stránku",
Expand Down
12 changes: 6 additions & 6 deletions demo/public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@
"Resources": "Resources",
"Previous screen": "Back to previous screen"
},
"AccessDeniedCard": {
"Access denied": "Access denied",
"Please contact application administrator for assigning appropriate access rights.": "Please contact application administrator for assigning appropriate access rights.",
"Silly as it sounds, redirection back to login failed": "Silly as it sounds, redirection back to login failed",
"Leave": "Leave"
},
"AuthHeaderDropdown": {
"My account": "My account",
"Access control": "Access control",
Expand Down Expand Up @@ -139,12 +145,6 @@
"Detail": "Detail",
"Advertised data": "Advertised data"
},
"TenantSelectionCard": {
"Select valid tenant to enter the application": "Select valid tenant to enter the application",
"Silly as it sounds, redirection back to login failed": "Silly as it sounds, redirection back to login failed",
"Select tenant": "Select tenant",
"Back to login": "Back to login"
},
"UserInterfaceCard": {
"User interface": "User interface",
"Build date": "Build date",
Expand Down
17 changes: 0 additions & 17 deletions doc/authorization.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,23 +28,6 @@ module.exports = {
}
```

### Set logout timeout

When user is not authorized by tenant, timeout period is set before logouting the user. During that period, SplashScreen is displayed to the user. Default value is 60000 aka 60s.

Example of setting/modifying the logout timeout period:

```
module.exports = {
app: {
...
authorizationLogoutTimeout: 120000,
...
},
}
```


## Sidebar item/child authorization with resource

This is an optional **softcheck** on user's resource and their ability to display particular item in the Sidebar navigation.
Expand Down
4 changes: 2 additions & 2 deletions doc/devconfig.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Developers can define as many properties they want to simulate e.g. features/ite

To successfuly simulate the `userinfo`, some properties should be set:

* `preffered_username` - username
* `username` - username
* `tenants` - list of tenants
* `roles` - list of roles
* `resources` - list of resources
Expand All @@ -54,7 +54,7 @@ module.exports = {
MOCK_USERINFO: {
"email": "[email protected]",
"phone_number": "0123456789",
"preferred_username": "Test",
"username": "Test",
"resources": ["test:testy:read"],
"roles": ["default/Gringo"],
"sub": "tst:123456789",
Expand Down
4 changes: 2 additions & 2 deletions src/containers/Application.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import HeaderService from '../services/HeaderService';
import SidebarService from './Sidebar/service';
import ThemeService from '../theme/ThemeService';

import TenantSelectionCard from '../modules/tenant/selector/TenantSelectionCard';
import AccessDeniedCard from '../modules/tenant/access/AccessDeniedCard';

import { ADD_ALERT, SET_ADVANCED_MODE, CHANGE_HELP_URL } from '../actions';

Expand Down Expand Up @@ -477,7 +477,7 @@ class Application extends Component {
<div className="app">
<Suspense fallback={<></>}>
<Alerts app={this} />
<TenantSelectionCard app={this} />
<AccessDeniedCard app={this} />
</Suspense>
<SplashScreen app={this} />
{this.Config.get('title') != null && this.Config.get('title') != undefined ?
Expand Down
15 changes: 13 additions & 2 deletions src/modules/auth/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export class SeaCatAuthApi {
this.App = app;

const scope = this.App.Config.get('seacat.auth.scope');
this.Scope = scope ? scope : "openid";
this.Scope = scope ? scope : "openid tenant userinfo:*";

this.ClientId = "asab-webui-auth";
this.ClientSecret = "TODO";
Expand All @@ -32,9 +32,20 @@ export class SeaCatAuthApi {

// This method will cause a navigation from the app to the OAuth2 login screen
async login(redirect_uri, force_login_prompt) {
/*
Adding tenant directly to scope (if available).
*/
let currentTenant = null;
if (this.App.Services.TenantService) {
currentTenant = this.App.Services.TenantService.get_current_tenant();
}
let scopeArray = this.Scope.split(" ");
let tenantValue = scopeArray.find(str => str.includes("tenant"));
let loginScope = (currentTenant != null) && (tenantValue != undefined) ? this.Scope.replace(`${tenantValue}`, `tenant:${currentTenant}`) : this.Scope;

const params = new URLSearchParams({
response_type: "code",
scope: this.Scope,
scope: loginScope,
client_id: this.ClientId,
redirect_uri: redirect_uri
});
Expand Down
7 changes: 5 additions & 2 deletions src/modules/auth/header.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,14 +90,17 @@ const mapStateToProps = state => {
if (userinfo.sub) {
sub = userinfo.sub;
} else {
sub = userinfo.id;
sub = userinfo.id;
}

if (userinfo.name) {
username = userinfo.name;
}
else if (userinfo.username) {
username = userinfo.username;
}
else if (userinfo.preferred_username) {
username = userinfo.preferred_username;
username = userinfo.preferred_username;
}
else {
username = userinfo.id;
Expand Down
36 changes: 26 additions & 10 deletions src/modules/auth/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ export default class AuthModule extends Module {
// Check the query string for 'code'
var qs = new URLSearchParams(window.location.search);
const authorization_code = qs.get('code');

// Checking error type in params
const errorType = qs.get('error');
if ((errorType != undefined) && errorType.includes("access_denied")) {
// If access has been denied, user will stay in splashscreen with Access denied card
return;
}

if (authorization_code !== null) {
await this._updateToken(authorization_code);
// Remove 'code' from a query string
Expand Down Expand Up @@ -68,7 +76,6 @@ export default class AuthModule extends Module {
// User info not found - go to login
sessionStorage.removeItem('SeaCatOAuth2Token');
let force_login_prompt = true;

await this.Api.login(this.RedirectURL, force_login_prompt);
return;
}
Expand All @@ -80,19 +87,16 @@ export default class AuthModule extends Module {
if (this.App.Config.get("authorization") !== "disabled" && this.App.Services.TenantService) {
// Tenant access validation
let tenantAuthorized = this.validateTenant();
let logoutTimeout = this.App.Config.get("authorizationLogoutTimeout") ? this.App.Config.get("authorizationLogoutTimeout") : 60000;
if (!tenantAuthorized) {
this.App.addAlert("danger", "ASABAuthModule|You are not authorized to use this application", logoutTimeout, true);
// Logout after some time
setTimeout(() => {
this.logout();
}, logoutTimeout);
// If tenant not authorized, redirect to Access denied card
let force_login_prompt = false;
await this.Api.login(this.RedirectURL, force_login_prompt);
return;
}
}

// Validate resources of items and children in navigation
if (this.App.Navigation.Items.length > 0) {
// Validate resources of items and children in navigation (resource validation depends on tenant)
if ((this.App.Navigation.Items.length > 0) && this.App.Services.TenantService) {
await this.validateNavigation();
}
if (this.UserInfo != null) {
Expand Down Expand Up @@ -130,7 +134,7 @@ export default class AuthModule extends Module {
MOCK_USERINFO: {
"email": "test",
"phone_number": "test",
"preferred_username": "test",
"username": "test",
"resources": ["test:testy:read"],
"roles": ["default/Gringo"],
"sub": "tst:123456789",
Expand Down Expand Up @@ -191,6 +195,18 @@ export default class AuthModule extends Module {
if (this.UserInfo !== null) {
let currentTenant = this.App.Services.TenantService.get_current_tenant();
resources = this.UserInfo.resources ? this.UserInfo.resources[currentTenant] : [];
/*
When switching between tenants,
we need to force authorization to obtain
correct data (resources) for particular
tenant (this is unavailable until auth token is updated)
*/
if (resources == undefined) {
let force_login_prompt = false;
await this.Api.login(this.RedirectURL, force_login_prompt);
return;
}

if (this.App.Store != null) {
this.App.Store.dispatch({ type: types.AUTH_RESOURCES, resources: resources });
}
Expand Down
81 changes: 81 additions & 0 deletions src/modules/tenant/access/AccessDeniedCard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { locationReplace } from 'asab-webui';
import { connect } from 'react-redux';

import {
Card, CardHeader, CardBody, CardTitle,
Button, Input, Label
} from 'reactstrap';

import "./access.scss";

function AccessDeniedCard(props) {
const { t } = useTranslation();
const [ accessDenied, setAccessDenied ] = useState(false);
const [ deniedTenant, setDeniedTenant ] = useState("");

useEffect(() => {
var qs = new URLSearchParams(window.location.search);
const errorType = qs.get('error');
if ((errorType != undefined) && errorType.includes("access_denied")) {
const tenantParameter = qs.get('tenant');
setAccessDenied(true);
setDeniedTenant(tenantParameter);
}
}, [])

/*
We can't use logout/redirection to login via /openidconnect/logout,
since we dont have a oauth token here (access denied)
*/
const continueToLogin = async () => {
const SeaCatAuthAPI = props.app.axiosCreate('seacat_auth');
let response;
try {
// Use public logout
response = await SeaCatAuthAPI.put('/public/logout');
} catch (err) {
console.error(err);
props.app.addAlert("danger", t("AccessDeniedCard|Silly as it sounds, the logout failed"));
}
await locationReplace(`${window.location.pathname}`);
}

return (
props.app.Services.TenantService &&
props.app.Modules.filter(obj => obj.Name === "AuthModule").length > 0 &&
(accessDenied == true) ?
<div className="access-denied-wrapper">
<Card className="access-denied-card shadow">
<CardHeader className="text-center border-bottom card-header-login">
<div className="card-header-title">
<CardTitle className="text-primary mb-0" tag="h2">
{t("AccessDeniedCard|Access denied")}
</CardTitle>
</div>
</CardHeader>
<CardBody>
<p className="text-center">
{t("AccessDeniedCard|Please contact application administrator for assigning appropriate access rights.")}
</p>
{deniedTenant &&
<p className="text-center tenant-text">
<span>Tenant: </span>{deniedTenant}
</p>}
<Button
className="justify-content-center"
block
color="primary"
onClick={() => {continueToLogin()}}
>
{t("AccessDeniedCard|Leave")}
</Button>
</CardBody>
</Card>
</div>
: null
);
}

export default AccessDeniedCard;
32 changes: 32 additions & 0 deletions src/modules/tenant/access/access.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
@import "~asab-webui/styles/constants/index.scss";

.access-denied-card {
max-width: 650px;
margin: 0 auto;
float: none;
margin-top: 1% !important;
margin-bottom: -15% !important;
background-color: $bg-color;
z-index: 999;
.card-body {
p {
margin-bottom: 8px;
}
};
.card-header-login {
min-height: inherit !important;
}
}

.access-denied-wrapper {
display: inline-block;
position: absolute;
width: 100%;
}

.tenant-text {
font-weight: 600;
span {
font-weight: initial;
}
}
Loading

0 comments on commit e7c9b7e

Please sign in to comment.