2021-03-11 22:51:58 +01:00
import { EventEmitter } from 'events' ;
2021-05-02 21:11:17 +02:00
import metricsHelper from '../util/metrics-helper' ;
2021-04-29 10:21:29 +02:00
import { DB_TIME } from '../metric-events' ;
2021-08-12 15:04:37 +02:00
import { Logger } from '../logger' ;
import {
2022-07-21 16:23:56 +02:00
IAccessInfo ,
2021-08-12 15:04:37 +02:00
IAccessStore ,
2023-08-17 09:43:43 +02:00
IProjectRoleUsage ,
2021-08-12 15:04:37 +02:00
IRole ,
2022-09-30 13:36:45 +02:00
IRoleWithProject ,
2021-08-12 15:04:37 +02:00
IUserPermission ,
2022-07-21 16:23:56 +02:00
IUserRole ,
2023-08-25 10:31:37 +02:00
IUserWithProjectRoles ,
2021-08-12 15:04:37 +02:00
} from '../types/stores/access-store' ;
2023-11-24 16:06:37 +01:00
import { IPermission , IUserAccessOverview , RoleType } from '../types/model' ;
2022-01-13 11:14:17 +01:00
import NotFoundError from '../error/notfound-error' ;
import {
ENVIRONMENT_PERMISSION_TYPE ,
2023-08-25 10:31:37 +02:00
PROJECT_ROLE_TYPES ,
2022-01-13 11:14:17 +01:00
ROOT_PERMISSION_TYPE ,
} from '../util/constants' ;
2023-01-30 09:02:44 +01:00
import { Db } from './db' ;
2023-09-19 16:15:27 +02:00
import {
IdPermissionRef ,
NamePermissionRef ,
PermissionRef ,
} from 'lib/services/access-service' ;
2023-10-06 13:38:32 +02:00
import { inTransaction } from './transaction' ;
2021-03-11 22:51:58 +01:00
const T = {
ROLE_USER : 'role_user' ,
ROLES : 'roles' ,
2022-07-21 16:23:56 +02:00
GROUPS : 'groups' ,
GROUP_ROLE : 'group_role' ,
GROUP_USER : 'group_user' ,
2021-03-11 22:51:58 +01:00
ROLE_PERMISSION : 'role_permission' ,
2022-01-13 11:14:17 +01:00
PERMISSIONS : 'permissions' ,
PERMISSION_TYPES : 'permission_types' ,
2022-11-14 14:05:26 +01:00
CHANGE_REQUEST_SETTINGS : 'change_request_settings' ,
2022-11-23 08:30:54 +01:00
PERSONAL_ACCESS_TOKENS : 'personal_access_tokens' ,
PUBLIC_SIGNUP_TOKENS_USER : 'public_signup_tokens_user' ,
2021-03-11 22:51:58 +01:00
} ;
2022-01-13 11:14:17 +01:00
interface IPermissionRow {
id : number ;
permission : string ;
display_name : string ;
environment? : string ;
type : string ;
project? : string ;
role_id : number ;
}
2023-11-27 12:12:09 +01:00
type NameAndIdPermission = NamePermissionRef & IdPermissionRef ;
2023-09-19 16:15:27 +02:00
2021-08-12 15:04:37 +02:00
export class AccessStore implements IAccessStore {
private logger : Logger ;
2021-03-11 22:51:58 +01:00
private timer : Function ;
2023-01-30 09:02:44 +01:00
private db : Db ;
2021-03-11 22:51:58 +01:00
2023-01-30 09:02:44 +01:00
constructor ( db : Db , eventBus : EventEmitter , getLogger : Function ) {
2021-03-11 22:51:58 +01:00
this . db = db ;
2021-08-12 15:04:37 +02:00
this . logger = getLogger ( 'access-store.ts' ) ;
2021-03-11 22:51:58 +01:00
this . timer = ( action : string ) = >
metricsHelper . wrapTimer ( eventBus , DB_TIME , {
store : 'access-store' ,
action ,
} ) ;
}
2023-11-27 12:12:09 +01:00
private permissionHasName = ( permission : PermissionRef ) : boolean = > {
return ( permission as NamePermissionRef ) . name !== undefined ;
2023-09-19 16:15:27 +02:00
} ;
2023-11-27 12:12:09 +01:00
private permissionIdsToNames = async (
permissions : IdPermissionRef [ ] ,
) : Promise < NameAndIdPermission [ ] > = > {
const permissionIds = ( permissions ? ? [ ] )
. filter ( ( p ) = > p . id !== undefined )
. map ( ( p ) = > p . id ) ;
2023-09-19 16:15:27 +02:00
2023-11-27 12:12:09 +01:00
if ( permissionIds . length === 0 ) {
2023-09-19 16:15:27 +02:00
return [ ] ;
}
2023-11-27 12:12:09 +01:00
const stopTimer = this . timer ( 'permissionIdsToNames' ) ;
2023-09-19 16:15:27 +02:00
const rows = await this . db
. select ( 'id' , 'permission' )
. from ( T . PERMISSIONS )
2023-11-27 12:12:09 +01:00
. whereIn ( 'id' , permissionIds ) ;
2023-09-19 16:15:27 +02:00
chore(deps): update dependency @biomejs/biome to v1.4.0 (#5288)
[![Mend Renovate logo
banner](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)
This PR contains the following updates:
| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [@biomejs/biome](https://biomejs.dev)
([source](https://togithub.com/biomejs/biome)) | [`1.3.3` ->
`1.4.0`](https://renovatebot.com/diffs/npm/@biomejs%2fbiome/1.3.3/1.4.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@biomejs%2fbiome/1.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@biomejs%2fbiome/1.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@biomejs%2fbiome/1.3.3/1.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@biomejs%2fbiome/1.3.3/1.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
---
### Release Notes
<details>
<summary>biomejs/biome (@​biomejs/biome)</summary>
###
[`v1.4.0`](https://togithub.com/biomejs/biome/blob/HEAD/CHANGELOG.md#140-2023-11-27)
[Compare
Source](https://togithub.com/biomejs/biome/compare/af24597c1877c7b5a96bb7cc59bab655a577116f...889593e3f983a6fec642d20eea3c7f94d58fc7e1)
##### CLI
- Remove the CLI options from the `lsp-proxy`, as they were never meant
to be passed to that command. Contributed by
[@​ematipico](https://togithub.com/ematipico)
- Add option `--config-path` to `lsp-proxy` and `start` commands. It's
now possible to tell the Daemon server to load `biome.json` from a
custom path. Contributed by
[@​ematipico](https://togithub.com/ematipico)
- Add new `--diagnostic-level` option to let users control the level of
diagnostics printed by the CLI. Possible values are: `"info"`, `"warn"`,
`"hint"`. Contributed by
[@​simonxabris](https://togithub.com/simonxabris)
- Add option `--line-feed` to the `format` command. Contributed by
[@​SuperchupuDev](https://togithub.com/SuperchupuDev)
- Add option `--bracket-same-line` to the `format` command. Contributed
by [@​faultyserve](https://togithub.com/faultyserve)
- Add option `--bracket-spacing` to the `format` command. Contributed by
[@​faultyserve](https://togithub.com/faultyserve)
##### Bug fixes
- Fix the command `format`, now it returns a non-zero exit code when if
there pending diffs. Contributed by
[@​ematipico](https://togithub.com/ematipico)
##### Configuration
- Add option `formatter.lineFeed`. Contributed by
[@​SuperchupuDev](https://togithub.com/SuperchupuDev)
- Add option `javascript.formatter.bracketSameLine`. Contributed by
[@​faultyserve](https://togithub.com/faultyserve)
- Add option `javascript.formatter.bracketSpacing`. Contributed by
[@​faultyserve](https://togithub.com/faultyserve)
##### Formatter
##### New features
- Add a new option
[`--line-ending`](https://biomejs.dev/reference/configuration/#formatterlineending).
This option allows changing the type of line endings. Contributed by
[@​SuperchupuDev](https://togithub.com/SuperchupuDev)
- Added a new option called `--bracket-spacing` to the formatter. This
option allows you to control whether spaces are inserted around the
brackets of object literals.
[#​627](https://togithub.com/biomejs/biome/issues/627).
Contributed by [@​faultyserver](https://togithub.com/faultyserver)
- Added a new option called `--bracket-same-line` to the formatter. This
option allows you to control whether spaces are inserted around the
brackets of object literals.
[#​627](https://togithub.com/biomejs/biome/issues/627).
Contributed by [@​faultyserver](https://togithub.com/faultyserver)
##### Bug fixes
- Fix [#​832](https://togithub.com/biomejs/biome/issues/832), the
formatter no longer keeps an unnecessary trailing comma in type
parameter lists. Contributed by
[@​Conaclos](https://togithub.com/Conaclos)
- Fix [#​301](https://togithub.com/biomejs/biome/issues/301), the
formatter should not break before the `in` keyword. Contributed by
[@​ematipico](https://togithub.com/ematipico)
##### Linter
##### Promoted rules
-
[a11y/noInteractiveElementToNoninteractiveRole](https://biomejs.dev/linter/rules/no-interactive-element-to-noninteractive-role)
-
[complexity/noThisInStatic](https://biomejs.dev/linter/rules/no-this-in-static)
-
[complexity/useArrowFunction](https://biomejs.dev/linter/rules/use-arrow-function)
-
[correctness/noEmptyCharacterClassInRegex](https://biomejs.dev/linter/rules/no-empty-character-class-in-regex)
-
[correctness/noInvalidNewBuiltin](https://biomejs.dev/linter/rules/no-invalid-new-builtin)
-
[style/noUselessElse](https://biomejs.dev/linter/rules/no-useless-else)
-
[style/useAsConstAssertion](https://biomejs.dev/linter/rules/use-as-const-assertion)
-
[style/useShorthandAssign](https://biomejs.dev/linter/rules/use-shorthand-assign)
-
[suspicious/noApproximativeNumericConstant](https://biomejs.dev/linter/rules/no-approximative-numeric-constant)
-
[suspicious/noMisleadingInstantiator](https://biomejs.dev/linter/rules/no-misleading-instantiator)
-
[suspicious/noMisrefactoredShorthandAssign](https://biomejs.dev/linter/rules/no-misrefactored-shorthand-assign)
The following rules are now recommended:
- [a11y/noAccessKey](https://biomejs.dev/linter/rules/no-access-key)
-
[a11y/useHeadingContent](https://biomejs.dev/linter/rules/use-heading-content)
-
[complexity/useSimpleNumberKeys](https://biomejs.dev/linter/use-simple-number-keys)
The following rules are now deprecated:
-
[correctness/noNewSymbol](https://biomejs.dev/linter/rules/no-new-symbol)
The rule is replaced by
[correctness/noInvalidNewBuiltin](https://biomejs.dev/linter/rules/no-invalid-new-builtin)
##### New features
- Add
[noDefaultExport](https://biomejs.dev/linter/rules/no-default-export)
which disallows `export default`. Contributed by
[@​Conaclos](https://togithub.com/Conaclos)
- Add
[noAriaHiddenOnFocusable](https://biomejs.dev/linter/rules/no-aria-hidden-on-focusable)
which reports hidden and focusable elements. Contributed by
[@​vasucp1207](https://togithub.com/vasucp1207)
- Add
[noImplicitAnyLet](https://biomejs.dev/linter/rules/no-implicit-any-let)
that reports variables declared with `let` and without initialization
and type annotation. Contributed by
[@​TaKO8Ki](https://togithub.com/TaKO8Ki) and
[@​b4s36t4](https://togithub.com/b4s36t4)
- Add [useAwait](https://biomejs.dev/linter/rules/use-await) that
reports `async` functions that don't use an `await` expression.
- Add
[useValidAriaRole](https://biomejs.dev/linter/rules/use-valid-aria-role).
Contributed by [@​vasucp1207](https://togithub.com/vasucp1207)
- Add [useRegexLiterals](https://biomejs.dev/linter/use-regex-literals)
that suggests turning call to the regex constructor into regex literals.
COntributed by [@​Yuiki](https://togithub.com/Yuiki)
##### Enhancements
- Add an unsafe code fix for
[a11y/useAriaActivedescendantWithTabindex](https://biomejs.dev/linter/rules/use-aria-activedescendant-with-tabindex)
##### Bug fixes
- Fix [#​639](https://togithub.com/biomejs/biome/issues/639) by
ignoring unused TypeScript's mapped key. Contributed by
[@​Conaclos](https://togithub.com/Conaclos)
- Fix [#​565](https://togithub.com/biomejs/biome/issues/565) by
handling several `infer` with the same name in extends clauses of
TypeScript's conditional types. Contributed by
[@​Conaclos](https://togithub.com/Conaclos)
- Fix [#​653](https://togithub.com/biomejs/biome/issues/653).
[noUnusedImports](https://biomejs.dev/linter/rules/no-unused-imports)
now correctly removes the entire line where the unused `import` is.
Contributed by [@​Conaclos](https://togithub.com/Conaclos)
- Fix [#​607](https://togithub.com/biomejs/biome/issues/609)
`useExhaustiveDependencies`, ignore optional chaining, Contributed by
[@​msdlisper](https://togithub.com/msdlisper)
- Fix [#​676](https://togithub.com/biomejs/biome/issues/676), by
using the correct node for the `"noreferrer"` when applying the code
action. Contributed by
[@​ematipico](https://togithub.com/ematipico)
- Fix [#​455](https://togithub.com/biomejs/biome/issues/455). The
CLI can now print complex emojis to the console correctly.
- Fix [#​727](https://togithub.com/biomejs/biome/issues/727).
[noInferrableTypes](https://biomejs.dev/linter/rules/no-inferrable-types)
now correctly keeps type annotations when the initialization expression
is `null`. Contributed by
[@​Conaclos](https://togithub.com/Conaclos)
- Fix [#​784](https://togithub.com/biomejs/biome/issues/784),
[noSvgWithoutTitle](https://biomejs.dev/linter/rules/no-svg-without-title)
fixes false-positives to `aria-label` and reports svg's role attribute
is implicit. Contributed by
[@​unvalley](https://togithub.com/unvalley)
- Fix [#​834](https://togithub.com/biomejs/biome/issues/834) that
made
[noUselessLoneBlockStatements](https://biomejs.dev/linter/rules/no-useless-lone-block-statements)
reports block statements of switch clauses. Contributed by
[@​vasucp1207](https://togithub.com/vasucp1207)
- Fix [#​783](https://togithub.com/biomejs/biome/issues/834) that
made
[noUselessLoneBlockStatements](https://biomejs.dev/linter/rules/no-useless-lone-block-statements)
reports block statements of `try-catch` structures. Contributed by
[@​hougesen](https://togithub.com/hougesen)
- Fix [#​69](https://togithub.com/biomejs/biome/issues/69) that
made
[correctness/noUnnecessaryContinue](https://biomejs.dev/linter/rules/no-unnecessary-continue)
incorrectly reports a `continue` used to break a switch clause.
Contributed by [@​TaKO8Ki](https://togithub.com/TaKO8Ki)
- Fix [#​664](https://togithub.com/biomejs/biome/issues/664) by
improving the diagnostic of
[style/useNamingConvention](https://biomejs.dev/linter/use-naming-convention)
when double capital are detected in strict camel case mode. Contributed
by [@​vasucp1207](https://togithub.com/vasucp1207)
- Fix [#​643](https://togithub.com/biomejs/biome/issues/643) that
erroneously parsed the option of
[complexity/useExhaustiveDependencies](https://biomejs.dev/linter/use-naming-convention).
Contributed by [@​arendjr](https://togithub.com/arendjr)
##### Parser
##### Bug fixes
- Fix [#​846](https://togithub.com/biomejs/biome/issues/846) that
erroneously parsed `<const T,>() => {}` as a JSX tag instead of an arrow
function when both TypeScript and JSX are enabled.
##### VSCode
</details>
---
### Configuration
📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).
🚦 **Automerge**: Enabled.
♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.
🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.
---
- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box
---
This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/Unleash/unleash).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy40Ni4wIiwidXBkYXRlZEluVmVyIjoiMzcuNTkuOCIsInRhcmdldEJyYW5jaCI6Im1haW4ifQ==-->
---------
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Christopher Kolstad <chriswk@getunleash.io>
2023-11-28 10:32:00 +01:00
const rowByPermissionId = rows . reduce (
( acc , row ) = > {
acc [ row . id ] = row ;
return acc ;
} ,
{ } as Map < string , IPermissionRow > ,
) ;
2023-09-19 16:15:27 +02:00
2023-11-27 12:12:09 +01:00
const permissionsWithNames = permissions . map ( ( permission ) = > ( {
name : rowByPermissionId [ permission . id ] . permission ,
2023-09-19 16:15:27 +02:00
. . . permission ,
} ) ) ;
stopTimer ( ) ;
2023-11-27 12:12:09 +01:00
return permissionsWithNames ;
2023-09-19 16:15:27 +02:00
} ;
resolvePermissions = async (
permissions : PermissionRef [ ] ,
2023-11-27 12:12:09 +01:00
) : Promise < NamePermissionRef [ ] > = > {
2023-09-19 16:15:27 +02:00
if ( permissions === undefined || permissions . length === 0 ) {
return [ ] ;
}
2023-11-27 12:12:09 +01:00
// permissions without names (just ids)
const permissionsWithoutNames = permissions . filter (
( p ) = > ! this . permissionHasName ( p ) ,
) as IdPermissionRef [ ] ;
if ( permissionsWithoutNames . length === permissions . length ) {
// all permissions without names
return await this . permissionIdsToNames ( permissionsWithoutNames ) ;
} else if ( permissionsWithoutNames . length === 0 ) {
// all permissions have names
return permissions as NamePermissionRef [ ] ;
2023-09-19 16:15:27 +02:00
}
2023-11-27 12:12:09 +01:00
// some permissions have names, some don't (should not happen!)
const namedPermissionsFromIds = await this . permissionIdsToNames (
permissionsWithoutNames ,
) ;
2023-09-19 16:15:27 +02:00
return permissions . map ( ( permission ) = > {
2023-11-27 12:12:09 +01:00
if ( this . permissionHasName ( permission ) ) {
return permission as NamePermissionRef ;
2023-09-19 16:15:27 +02:00
} else {
2023-11-27 12:12:09 +01:00
return namedPermissionsFromIds . find (
( p ) = > p . id === ( permission as IdPermissionRef ) . id ,
2023-09-19 16:15:27 +02:00
) ! ;
}
} ) ;
} ;
2021-08-12 15:04:37 +02:00
async delete ( key : number ) : Promise < void > {
await this . db ( T . ROLES ) . where ( { id : key } ) . del ( ) ;
}
async deleteAll ( ) : Promise < void > {
await this . db ( T . ROLES ) . del ( ) ;
}
destroy ( ) : void { }
async exists ( key : number ) : Promise < boolean > {
const result = await this . db . raw (
2022-07-21 16:23:56 +02:00
` SELECT EXISTS(SELECT 1 FROM ${ T . ROLES } WHERE id = ?) AS present ` ,
2021-08-12 15:04:37 +02:00
[ key ] ,
) ;
const { present } = result . rows [ 0 ] ;
return present ;
}
async get ( key : number ) : Promise < IRole > {
2022-01-13 11:14:17 +01:00
const role = await this . db
2021-08-12 15:04:37 +02:00
. select ( [ 'id' , 'name' , 'type' , 'description' ] )
. where ( 'id' , key )
. first ( )
. from < IRole > ( T . ROLES ) ;
2022-01-13 11:14:17 +01:00
if ( ! role ) {
throw new NotFoundError ( ` Could not find role with id: ${ key } ` ) ;
}
return role ;
2021-08-12 15:04:37 +02:00
}
async getAll ( ) : Promise < IRole [ ] > {
return Promise . resolve ( [ ] ) ;
}
2022-01-13 11:14:17 +01:00
async getAvailablePermissions ( ) : Promise < IPermission [ ] > {
const rows = await this . db
. select ( [ 'id' , 'permission' , 'type' , 'display_name' ] )
. where ( 'type' , 'project' )
. orWhere ( 'type' , 'environment' )
2023-06-14 15:40:40 +02:00
. orWhere ( 'type' , 'root' )
2022-01-13 11:14:17 +01:00
. from ( ` ${ T . PERMISSIONS } as p ` ) ;
2023-06-30 12:15:11 +02:00
return rows . map ( this . mapPermission ) ;
2022-01-13 11:14:17 +01:00
}
mapPermission ( permission : IPermissionRow ) : IPermission {
return {
id : permission.id ,
name : permission.permission ,
displayName : permission.display_name ,
type : permission . type ,
} ;
}
2021-04-12 20:25:03 +02:00
async getPermissionsForUser ( userId : number ) : Promise < IUserPermission [ ] > {
2021-03-11 22:51:58 +01:00
const stopTimer = this . timer ( 'getPermissionsForUser' ) ;
2022-07-21 16:23:56 +02:00
let userPermissionQuery = this . db
2022-01-13 11:14:17 +01:00
. select (
'project' ,
2023-11-27 12:12:09 +01:00
'rp.permission' ,
2022-01-13 11:14:17 +01:00
'environment' ,
'type' ,
'ur.role_id' ,
)
. from < IPermissionRow > ( ` ${ T . ROLE_PERMISSION } AS rp ` )
. join ( ` ${ T . ROLE_USER } AS ur ` , 'ur.role_id' , 'rp.role_id' )
2023-11-27 12:12:09 +01:00
. join ( ` ${ T . PERMISSIONS } AS p ` , 'p.permission' , 'rp.permission' )
2021-04-12 20:25:03 +02:00
. where ( 'ur.user_id' , '=' , userId ) ;
2022-07-21 16:23:56 +02:00
2022-08-26 08:22:42 +02:00
userPermissionQuery = userPermissionQuery . union ( ( db ) = > {
db . select (
'project' ,
2023-11-27 12:12:09 +01:00
'rp.permission' ,
2022-08-26 08:22:42 +02:00
'environment' ,
'p.type' ,
'gr.role_id' ,
)
. from < IPermissionRow > ( ` ${ T . GROUP_USER } AS gu ` )
. join ( ` ${ T . GROUPS } AS g ` , 'g.id' , 'gu.group_id' )
. join ( ` ${ T . GROUP_ROLE } AS gr ` , 'gu.group_id' , 'gr.group_id' )
. join ( ` ${ T . ROLE_PERMISSION } AS rp ` , 'rp.role_id' , 'gr.role_id' )
2023-11-27 12:12:09 +01:00
. join ( ` ${ T . PERMISSIONS } AS p ` , 'p.permission' , 'rp.permission' )
2023-04-20 12:29:30 +02:00
. andWhere ( 'gu.user_id' , '=' , userId ) ;
2022-08-26 08:22:42 +02:00
} ) ;
2023-04-20 12:29:30 +02:00
userPermissionQuery = userPermissionQuery . union ( ( db ) = > {
db . select (
this . db . raw ( "'default' as project" ) ,
2023-11-27 12:12:09 +01:00
'rp.permission' ,
2023-04-20 12:29:30 +02:00
'environment' ,
'p.type' ,
'g.root_role_id as role_id' ,
)
. from < IPermissionRow > ( ` ${ T . GROUP_USER } as gu ` )
. join ( ` ${ T . GROUPS } AS g ` , 'g.id' , 'gu.group_id' )
. join (
` ${ T . ROLE_PERMISSION } as rp ` ,
'rp.role_id' ,
'g.root_role_id' ,
)
2023-11-27 12:12:09 +01:00
. join ( ` ${ T . PERMISSIONS } as p ` , 'p.permission' , 'rp.permission' )
2023-04-20 12:29:30 +02:00
. whereNotNull ( 'g.root_role_id' )
. andWhere ( 'gu.user_id' , '=' , userId ) ;
} ) ;
2022-07-21 16:23:56 +02:00
const rows = await userPermissionQuery ;
2021-03-11 22:51:58 +01:00
stopTimer ( ) ;
2022-01-13 11:14:17 +01:00
return rows . map ( this . mapUserPermission ) ;
2021-03-11 22:51:58 +01:00
}
2022-01-13 11:14:17 +01:00
mapUserPermission ( row : IPermissionRow ) : IUserPermission {
2023-06-14 15:40:40 +02:00
let project : string | undefined = undefined ;
2022-01-13 11:14:17 +01:00
// Since the editor should have access to the default project,
// we map the project to the project and environment specific
// permissions that are connected to the editor role.
if ( row . type !== ROOT_PERMISSION_TYPE ) {
project = row . project ;
}
2021-03-11 22:51:58 +01:00
2022-01-13 11:14:17 +01:00
const environment =
row . type === ENVIRONMENT_PERMISSION_TYPE
? row . environment
: undefined ;
2021-03-11 22:51:58 +01:00
2022-07-21 16:23:56 +02:00
return {
2022-01-13 11:14:17 +01:00
project ,
environment ,
permission : row.permission ,
} ;
2021-03-11 22:51:58 +01:00
}
2022-01-13 11:14:17 +01:00
async getPermissionsForRole ( roleId : number ) : Promise < IPermission [ ] > {
const stopTimer = this . timer ( 'getPermissionsForRole' ) ;
const rows = await this . db
. select (
'p.id' ,
2023-11-27 12:12:09 +01:00
'rp.permission' ,
2022-01-13 11:14:17 +01:00
'rp.environment' ,
'p.display_name' ,
'p.type' ,
)
. from < IPermission > ( ` ${ T . ROLE_PERMISSION } as rp ` )
2023-11-27 12:12:09 +01:00
. join ( ` ${ T . PERMISSIONS } as p ` , 'p.permission' , 'rp.permission' )
2022-01-13 11:14:17 +01:00
. where ( 'rp.role_id' , '=' , roleId ) ;
stopTimer ( ) ;
return rows . map ( ( permission ) = > {
return {
id : permission.id ,
name : permission.permission ,
environment : permission.environment ,
displayName : permission.display_name ,
type : permission . type ,
} ;
} ) ;
2021-03-11 22:51:58 +01:00
}
2022-01-13 11:14:17 +01:00
async addEnvironmentPermissionsToRole (
role_id : number ,
2023-09-19 16:15:27 +02:00
permissions : PermissionRef [ ] ,
2022-01-13 11:14:17 +01:00
) : Promise < void > {
2023-11-27 12:12:09 +01:00
const resolvedPermissions = await this . resolvePermissions ( permissions ) ;
2023-09-19 16:15:27 +02:00
2023-11-27 12:12:09 +01:00
const rows = resolvedPermissions . map ( ( permission ) = > {
2022-01-13 11:14:17 +01:00
return {
role_id ,
2023-11-27 12:12:09 +01:00
permission : permission.name ,
2022-01-13 11:14:17 +01:00
environment : permission.environment ,
} ;
} ) ;
2022-04-01 11:10:21 +02:00
await this . db . batchInsert ( T . ROLE_PERMISSION , rows ) ;
2021-04-09 13:46:53 +02:00
}
2022-01-13 11:14:17 +01:00
async unlinkUserRoles ( userId : number ) : Promise < void > {
return this . db ( T . ROLE_USER )
2021-03-11 22:51:58 +01:00
. where ( {
2022-01-13 11:14:17 +01:00
user_id : userId ,
2021-03-11 22:51:58 +01:00
} )
. delete ( ) ;
}
2022-11-23 08:30:54 +01:00
async unlinkUserGroups ( userId : number ) : Promise < void > {
return this . db ( T . GROUP_USER )
. where ( {
user_id : userId ,
} )
. delete ( ) ;
}
async clearUserPersonalAccessTokens ( userId : number ) : Promise < void > {
return this . db ( T . PERSONAL_ACCESS_TOKENS )
. where ( {
user_id : userId ,
} )
. delete ( ) ;
}
async clearPublicSignupUserTokens ( userId : number ) : Promise < void > {
return this . db ( T . PUBLIC_SIGNUP_TOKENS_USER )
. where ( {
user_id : userId ,
} )
. delete ( ) ;
}
2022-07-21 16:23:56 +02:00
async getProjectUsersForRole (
2022-01-13 11:14:17 +01:00
roleId : number ,
projectId? : string ,
2022-07-21 16:23:56 +02:00
) : Promise < IUserRole [ ] > {
2022-01-13 11:14:17 +01:00
const rows = await this . db
2022-07-21 16:23:56 +02:00
. select ( [ 'user_id' , 'ru.created_at' ] )
2022-01-13 11:14:17 +01:00
. from < IRole > ( ` ${ T . ROLE_USER } AS ru ` )
. join ( ` ${ T . ROLES } as r ` , 'ru.role_id' , 'id' )
. where ( 'r.id' , roleId )
. andWhere ( 'ru.project' , projectId ) ;
2022-07-21 16:23:56 +02:00
return rows . map ( ( r ) = > ( {
userId : r.user_id ,
2023-11-24 16:06:37 +01:00
roleId ,
2022-07-21 16:23:56 +02:00
addedAt : r.created_at ,
} ) ) ;
2022-01-13 11:14:17 +01:00
}
2023-08-25 10:31:37 +02:00
async getProjectUsers (
projectId? : string ,
) : Promise < IUserWithProjectRoles [ ] > {
const rows = await this . db
. select ( [ 'user_id' , 'ru.created_at' , 'ru.role_id' ] )
. from < IRole > ( ` ${ T . ROLE_USER } AS ru ` )
. join ( ` ${ T . ROLES } as r ` , 'ru.role_id' , 'id' )
. whereIn ( 'r.type' , PROJECT_ROLE_TYPES )
. andWhere ( 'ru.project' , projectId ) ;
return rows . reduce ( ( acc , row ) = > {
const existingUser = acc . find ( ( user ) = > user . id === row . user_id ) ;
if ( existingUser ) {
existingUser . roles . push ( row . role_id ) ;
} else {
acc . push ( {
id : row.user_id ,
addedAt : row.created_at ,
roleId : row.role_id ,
roles : [ row . role_id ] ,
} ) ;
}
return acc ;
} , [ ] ) ;
}
2022-09-30 13:36:45 +02:00
async getRolesForUserId ( userId : number ) : Promise < IRoleWithProject [ ] > {
2021-03-11 22:51:58 +01:00
return this . db
. select ( [ 'id' , 'name' , 'type' , 'project' , 'description' ] )
. from < IRole [ ] > ( T . ROLES )
. innerJoin ( ` ${ T . ROLE_USER } as ru ` , 'ru.role_id' , 'id' )
. where ( 'ru.user_id' , '=' , userId ) ;
}
2023-11-24 16:06:37 +01:00
async getRootRoleForUser ( userId : number ) : Promise < IRole | undefined > {
return this . db
. select ( [ 'id' , 'name' , 'type' , 'description' ] )
. from < IRole [ ] > ( T . ROLES )
. innerJoin ( ` ${ T . ROLE_USER } as ru ` , 'ru.role_id' , 'id' )
. where ( 'ru.user_id' , '=' , userId )
. andWhere ( 'type' , '=' , RoleType . ROOT )
. first ( ) ;
}
2021-08-12 15:04:37 +02:00
async getUserIdsForRole ( roleId : number ) : Promise < number [ ] > {
2021-03-11 22:51:58 +01:00
const rows = await this . db
. select ( [ 'user_id' ] )
. from < IRole > ( T . ROLE_USER )
. where ( 'role_id' , roleId ) ;
2021-08-12 15:04:37 +02:00
return rows . map ( ( r ) = > r . user_id ) ;
2021-03-11 22:51:58 +01:00
}
2023-08-08 13:45:19 +02:00
async getGroupIdsForRole ( roleId : number ) : Promise < number [ ] > {
const rows = await this . db
. select ( [ 'group_id' ] )
. from < IRole > ( T . GROUP_ROLE )
. where ( 'role_id' , roleId ) ;
return rows . map ( ( r ) = > r . group_id ) ;
}
2023-08-17 09:43:43 +02:00
async getProjectUserAndGroupCountsForRole (
roleId : number ,
) : Promise < IProjectRoleUsage [ ] > {
const query = await this . db . raw (
`
2023-08-25 10:31:37 +02:00
SELECT
2023-08-17 09:43:43 +02:00
uq . project ,
sum ( uq . user_count ) AS user_count ,
sum ( uq . svc_account_count ) AS svc_account_count ,
sum ( uq . group_count ) AS group_count
FROM (
2023-08-25 10:31:37 +02:00
SELECT
2023-08-17 09:43:43 +02:00
project ,
0 AS user_count ,
0 AS svc_account_count ,
count ( project ) AS group_count
FROM group_role
WHERE role_id = ?
GROUP BY project
UNION SELECT
project ,
count ( us . id ) AS user_count ,
count ( svc . id ) AS svc_account_count ,
0 AS group_count
FROM role_user AS usr_r
LEFT OUTER JOIN public . users AS us ON us . id = usr_r . user_id AND us . is_service = 'false'
LEFT OUTER JOIN public . users AS svc ON svc . id = usr_r . user_id AND svc . is_service = 'true'
WHERE usr_r . role_id = ?
GROUP BY usr_r . project
) AS uq
GROUP BY uq . project
` ,
[ roleId , roleId ] ,
) ;
return query . rows . map ( ( r ) = > {
return {
project : r.project ,
role : roleId ,
userCount : Number ( r . user_count ) ,
groupCount : Number ( r . group_count ) ,
serviceAccountCount : Number ( r . svc_account_count ) ,
} ;
} ) ;
}
2022-01-13 11:14:17 +01:00
async addUserToRole (
userId : number ,
roleId : number ,
2022-07-21 16:23:56 +02:00
projectId? : string ,
2022-01-13 11:14:17 +01:00
) : Promise < void > {
2023-11-16 12:03:27 +01:00
await this . db ( T . ROLE_USER )
. insert ( {
user_id : userId ,
role_id : roleId ,
project : projectId ,
} )
. onConflict ( [ 'user_id' , 'role_id' , 'project' ] )
. ignore ( ) ;
2021-03-11 22:51:58 +01:00
}
2022-01-13 11:14:17 +01:00
async removeUserFromRole (
userId : number ,
roleId : number ,
projectId? : string ,
) : Promise < void > {
2021-03-11 22:51:58 +01:00
return this . db ( T . ROLE_USER )
. where ( {
user_id : userId ,
role_id : roleId ,
2022-01-13 11:14:17 +01:00
project : projectId ,
2021-03-11 22:51:58 +01:00
} )
. delete ( ) ;
}
2022-07-21 16:23:56 +02:00
async addGroupToRole (
groupId : number ,
roleId : number ,
createdBy : string ,
projectId? : string ,
) : Promise < void > {
return this . db ( T . GROUP_ROLE ) . insert ( {
group_id : groupId ,
role_id : roleId ,
project : projectId ,
created_by : createdBy ,
} ) ;
}
async removeGroupFromRole (
groupId : number ,
roleId : number ,
projectId? : string ,
) : Promise < void > {
return this . db ( T . GROUP_ROLE )
. where ( {
group_id : groupId ,
role_id : roleId ,
project : projectId ,
} )
. delete ( ) ;
}
2022-02-21 14:39:59 +01:00
async updateUserProjectRole (
userId : number ,
roleId : number ,
projectId : string ,
) : Promise < void > {
return this . db ( T . ROLE_USER )
. where ( {
user_id : userId ,
project : projectId ,
} )
2022-05-26 16:20:36 +02:00
. whereNotIn (
'role_id' ,
this . db ( T . ROLES ) . select ( 'id as role_id' ) . where ( 'type' , 'root' ) ,
)
2022-02-21 14:39:59 +01:00
. update ( 'role_id' , roleId ) ;
}
2022-07-21 16:23:56 +02:00
updateGroupProjectRole (
groupId : number ,
roleId : number ,
projectId : string ,
) : Promise < void > {
return this . db ( T . GROUP_ROLE )
. where ( {
group_id : groupId ,
project : projectId ,
} )
. whereNotIn (
'role_id' ,
this . db ( T . ROLES ) . select ( 'id as role_id' ) . where ( 'type' , 'root' ) ,
)
. update ( 'role_id' , roleId ) ;
}
2023-08-25 10:31:37 +02:00
async addRoleAccessToProject (
2022-07-21 16:23:56 +02:00
users : IAccessInfo [ ] ,
groups : IAccessInfo [ ] ,
projectId : string ,
roleId : number ,
createdBy : string ,
) : Promise < void > {
const userRows = users . map ( ( user ) = > {
return {
user_id : user.id ,
project : projectId ,
role_id : roleId ,
} ;
} ) ;
const groupRows = groups . map ( ( group ) = > {
return {
group_id : group.id ,
project : projectId ,
role_id : roleId ,
created_by : createdBy ,
} ;
} ) ;
2023-10-06 13:38:32 +02:00
await inTransaction ( this . db , async ( tx ) = > {
2022-07-21 16:23:56 +02:00
if ( userRows . length > 0 ) {
await tx ( T . ROLE_USER )
. insert ( userRows )
. onConflict ( [ 'project' , 'role_id' , 'user_id' ] )
. merge ( ) ;
}
if ( groupRows . length > 0 ) {
await tx ( T . GROUP_ROLE )
. insert ( groupRows )
. onConflict ( [ 'project' , 'role_id' , 'group_id' ] )
. merge ( ) ;
}
} ) ;
}
2023-08-25 10:31:37 +02:00
async addAccessToProject (
roles : number [ ] ,
groups : number [ ] ,
users : number [ ] ,
projectId : string ,
createdBy : string ,
) : Promise < void > {
const validatedProjectRoleIds = await this . db ( T . ROLES )
. select ( 'id' )
. whereIn ( 'id' , roles )
. whereIn ( 'type' , PROJECT_ROLE_TYPES )
. pluck ( 'id' ) ;
const groupRows = groups . flatMap ( ( group ) = >
validatedProjectRoleIds . map ( ( role ) = > ( {
group_id : group ,
project : projectId ,
role_id : role ,
created_by : createdBy ,
} ) ) ,
) ;
const userRows = users . flatMap ( ( user ) = >
validatedProjectRoleIds . map ( ( role ) = > ( {
user_id : user ,
project : projectId ,
role_id : role ,
} ) ) ,
) ;
2023-10-06 13:38:32 +02:00
await inTransaction ( this . db , async ( tx ) = > {
2023-08-25 10:31:37 +02:00
if ( groupRows . length > 0 ) {
await tx ( T . GROUP_ROLE )
. insert ( groupRows )
. onConflict ( [ 'project' , 'role_id' , 'group_id' ] )
. merge ( ) ;
}
if ( userRows . length > 0 ) {
await tx ( T . ROLE_USER )
. insert ( userRows )
. onConflict ( [ 'project' , 'role_id' , 'user_id' ] )
. merge ( ) ;
}
} ) ;
}
async setProjectRolesForUser (
projectId : string ,
userId : number ,
roles : number [ ] ,
) : Promise < void > {
const projectRoleIds = await this . db ( T . ROLES )
. select ( 'id' )
. whereIn ( 'type' , PROJECT_ROLE_TYPES )
. pluck ( 'id' ) ;
const projectRoleIdsSet = new Set ( projectRoleIds ) ;
const userRows = roles
. filter ( ( role ) = > projectRoleIdsSet . has ( role ) )
. map ( ( role ) = > ( {
user_id : userId ,
project : projectId ,
role_id : role ,
} ) ) ;
2023-10-06 13:38:32 +02:00
await inTransaction ( this . db , async ( tx ) = > {
2023-08-25 10:31:37 +02:00
await tx ( T . ROLE_USER )
. where ( 'project' , projectId )
. andWhere ( 'user_id' , userId )
. whereIn ( 'role_id' , projectRoleIds )
. delete ( ) ;
if ( userRows . length > 0 ) {
await tx ( T . ROLE_USER )
. insert ( userRows )
. onConflict ( [ 'project' , 'role_id' , 'user_id' ] )
. ignore ( ) ;
}
} ) ;
}
async getProjectRolesForUser (
projectId : string ,
userId : number ,
) : Promise < number [ ] > {
const rows = await this . db ( ` ${ T . ROLE_USER } as ru ` )
. join ( ` ${ T . ROLES } as r ` , 'ru.role_id' , 'r.id' )
. select ( 'ru.role_id' )
. where ( 'ru.project' , projectId )
. whereIn ( 'r.type' , PROJECT_ROLE_TYPES )
. andWhere ( 'ru.user_id' , userId ) ;
return rows . map ( ( r ) = > r . role_id as number ) ;
}
async setProjectRolesForGroup (
projectId : string ,
groupId : number ,
roles : number [ ] ,
createdBy : string ,
) : Promise < void > {
const projectRoleIds = await this . db ( T . ROLES )
. select ( 'id' )
. whereIn ( 'type' , PROJECT_ROLE_TYPES )
. pluck ( 'id' ) ;
const projectRoleIdsSet = new Set ( projectRoleIds ) ;
const groupRows = roles
. filter ( ( role ) = > projectRoleIdsSet . has ( role ) )
. map ( ( role ) = > ( {
group_id : groupId ,
project : projectId ,
role_id : role ,
created_by : createdBy ,
} ) ) ;
2023-10-06 13:38:32 +02:00
await inTransaction ( this . db , async ( tx ) = > {
2023-08-25 10:31:37 +02:00
await tx ( T . GROUP_ROLE )
. where ( 'project' , projectId )
. andWhere ( 'group_id' , groupId )
. whereIn ( 'role_id' , projectRoleIds )
. delete ( ) ;
if ( groupRows . length > 0 ) {
await tx ( T . GROUP_ROLE )
. insert ( groupRows )
. onConflict ( [ 'project' , 'role_id' , 'group_id' ] )
. ignore ( ) ;
}
} ) ;
}
async getProjectRolesForGroup (
projectId : string ,
groupId : number ,
) : Promise < number [ ] > {
const rows = await this . db ( ` ${ T . GROUP_ROLE } as gr ` )
. join ( ` ${ T . ROLES } as r ` , 'gr.role_id' , 'r.id' )
. select ( 'gr.role_id' )
. where ( 'gr.project' , projectId )
. whereIn ( 'r.type' , PROJECT_ROLE_TYPES )
. andWhere ( 'gr.group_id' , groupId ) ;
return rows . map ( ( row ) = > row . role_id as number ) ;
}
async removeUserAccess ( projectId : string , userId : number ) : Promise < void > {
return this . db ( T . ROLE_USER )
. where ( {
user_id : userId ,
project : projectId ,
} )
. whereIn (
'role_id' ,
this . db ( T . ROLES )
. select ( 'id as role_id' )
. whereIn ( 'type' , PROJECT_ROLE_TYPES ) ,
)
. delete ( ) ;
}
async removeGroupAccess ( projectId : string , groupId : number ) : Promise < void > {
return this . db ( T . GROUP_ROLE )
. where ( {
group_id : groupId ,
project : projectId ,
} )
. whereIn (
'role_id' ,
this . db ( T . ROLES )
. select ( 'id as role_id' )
. whereIn ( 'type' , PROJECT_ROLE_TYPES ) ,
)
. delete ( ) ;
}
2021-04-09 13:46:53 +02:00
async removeRolesOfTypeForUser (
userId : number ,
2023-06-14 15:40:40 +02:00
roleTypes : string [ ] ,
2021-04-09 13:46:53 +02:00
) : Promise < void > {
2023-11-06 14:38:12 +01:00
const rolesToRemove = await this . db ( T . ROLES )
2021-04-09 13:46:53 +02:00
. select ( 'id' )
2023-11-06 14:38:12 +01:00
. whereIn ( 'type' , roleTypes )
. pluck ( 'id' ) ;
2021-04-09 13:46:53 +02:00
return this . db ( T . ROLE_USER )
. where ( { user_id : userId } )
. whereIn ( 'role_id' , rolesToRemove )
. delete ( ) ;
}
2021-03-11 22:51:58 +01:00
async addPermissionsToRole (
role_id : number ,
2023-09-19 16:15:27 +02:00
permissions : PermissionRef [ ] | string [ ] ,
2022-01-13 11:14:17 +01:00
environment? : string ,
2021-03-11 22:51:58 +01:00
) : Promise < void > {
2023-09-19 16:15:27 +02:00
const permissionsAsRefs = ( permissions ? ? [ ] ) . map ( ( p ) = > {
if ( typeof p === 'string' ) {
return { name : p } ;
} else {
return p ;
}
} ) ;
// no need to pass down the environment in this particular case because it'll be overriden
2023-11-27 13:42:58 +01:00
const permissionsWithNames =
await this . resolvePermissions ( permissionsAsRefs ) ;
2022-01-13 11:14:17 +01:00
2023-11-27 12:12:09 +01:00
const newRoles = permissionsWithNames . map ( ( p ) = > ( {
2021-03-11 22:51:58 +01:00
role_id ,
2022-01-13 11:14:17 +01:00
environment ,
2023-11-27 12:12:09 +01:00
permission : p.name ,
2021-03-11 22:51:58 +01:00
} ) ) ;
2022-01-13 11:14:17 +01:00
return this . db . batchInsert ( T . ROLE_PERMISSION , newRoles ) ;
2021-03-11 22:51:58 +01:00
}
async removePermissionFromRole (
2022-01-13 11:14:17 +01:00
role_id : number ,
2021-03-11 22:51:58 +01:00
permission : string ,
2022-01-13 11:14:17 +01:00
environment? : string ,
2021-03-11 22:51:58 +01:00
) : Promise < void > {
return this . db ( T . ROLE_PERMISSION )
. where ( {
2022-01-13 11:14:17 +01:00
role_id ,
2023-11-27 12:12:09 +01:00
permission ,
2022-01-13 11:14:17 +01:00
environment ,
2021-03-11 22:51:58 +01:00
} )
. delete ( ) ;
}
2021-04-09 13:46:53 +02:00
2022-01-13 11:14:17 +01:00
async wipePermissionsFromRole ( role_id : number ) : Promise < void > {
return this . db ( T . ROLE_PERMISSION )
. where ( {
role_id ,
} )
. delete ( ) ;
2021-04-09 13:46:53 +02:00
}
2022-10-28 11:27:11 +02:00
async cloneEnvironmentPermissions (
sourceEnvironment : string ,
destinationEnvironment : string ,
) : Promise < void > {
return this . db . raw (
` insert into role_permission
2023-11-27 12:12:09 +01:00
( role_id , permission , environment )
( select role_id , permission , ?
2022-10-28 11:27:11 +02:00
from $ { T . ROLE_PERMISSION } where environment = ? ) ` ,
[ destinationEnvironment , sourceEnvironment ] ,
) ;
}
2023-09-14 11:43:39 +02:00
async getUserAccessOverview ( ) : Promise < IUserAccessOverview [ ] > {
2023-09-29 14:18:21 +02:00
const result = await this . db . raw ( ` SELECT u.id, u.created_at, u.name, u.email, u.seen_at, up.p_array as projects, gr.p_array as groups, gp.p_array as group_projects, r.name as root_role
2023-09-14 11:43:39 +02:00
FROM users u , LATERAL (
SELECT ARRAY (
SELECT ru . project
FROM role_user ru
WHERE ru . user_id = u . id
) AS p_array
) up , LATERAL (
SELECT r . name
FROM role_user ru
2023-09-15 12:10:16 +02:00
INNER JOIN roles r on ru . role_id = r . id
WHERE ru . user_id = u . id and r . type = 'root'
2023-09-14 11:43:39 +02:00
) r , LATERAL (
SELECT ARRAY (
2023-09-15 12:10:16 +02:00
SELECT g . name FROM group_user gu
JOIN groups g on g . id = gu . group_id
2023-09-14 11:43:39 +02:00
WHERE gu . user_id = u . id
) AS p_array
2023-09-15 12:10:16 +02:00
) gr , LATERAL (
SELECT ARRAY (
SELECT gr . project
FROM group_user gu
JOIN group_role gr ON gu . group_id = gr . group_id
WHERE gu . user_id = u . id
)
AS p_array
) gp
2023-09-14 11:43:39 +02:00
order by u . id ; ` );
return result . rows . map ( ( row ) = > {
return {
userId : row.id ,
createdAt : row.created_at ,
userName : row.name ,
userEmail : row.email ,
lastSeen : row.seen_at ,
accessibleProjects : row.projects ,
groups : row.groups ,
rootRole : row.root_role ,
2023-09-15 12:10:16 +02:00
groupProjects : row.group_projects ,
2023-09-14 11:43:39 +02:00
} ;
} ) ;
}
2021-03-11 22:51:58 +01:00
}