Skip to content

Commit

Permalink
feat(auth): Add domain to api scope, remove from scope group. (#8645)
Browse files Browse the repository at this point in the history
* Add domain to api scope, remove from scope group.

* Revert api group changes. Add missing migration.

* Parse domain name from scope. Remove default value

* Refactor and fix domain name checks.

Co-authored-by: Valur Einarsson <[email protected]>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored Oct 17, 2022
1 parent 78bbee6 commit 2a0c1be
Show file tree
Hide file tree
Showing 15 changed files with 197 additions and 62 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,13 @@ interface FormOutput {
}

const ApiScopeCreateForm: React.FC<Props> = (props) => {
const {
register,
handleSubmit,
errors,
formState,
setValue,
} = useForm<FormOutput>()
const { register, handleSubmit, errors, formState } = useForm<FormOutput>()
const { isSubmitting } = formState
const [isEditing, setIsEditing] = useState<boolean>(false)
const [available, setAvailable] = useState<boolean>(false)
const [groups, setGroups] = useState<ApiScopeGroup[]>([])
const [nameLength, setNameLength] = useState(0)
const [domains, setDomains] = useState<Domain[]>([])
const [domainIsTouched, setDomainIsTouched] = useState<boolean>(false)
//#region hint-box
const [
apiScopeNameHintVisible,
Expand Down Expand Up @@ -117,12 +110,6 @@ const ApiScopeCreateForm: React.FC<Props> = (props) => {
}
}

const onDomainChange = async (e: React.ChangeEvent<HTMLSelectElement>) => {
setDomainIsTouched(true)
setValue('apiScope.name', e.currentTarget.value)
document.getElementById('apiScope.name').focus()
}

return (
<div className="api-scope-form">
<div className="api-scope-form__wrapper">
Expand All @@ -132,39 +119,6 @@ const ApiScopeCreateForm: React.FC<Props> = (props) => {
<div className="api-scope-form__help">{localization.help}</div>
<form onSubmit={handleSubmit(save)}>
<div className="api-scope-form__container__fields">
<div
className={`api-scope-form__container__field${
isEditing ? ' hidden' : ''
}`}
>
<label htmlFor="domain" className="api-scope-form__label">
Domain
</label>
<select
id="domain"
name="domain"
onChange={(e) => onDomainChange(e)}
>
<option value={'null'} disabled={true} selected>
{localization.fields['domain'].selectAnItem}
</option>
{domains.map((domain: Domain) => {
return (
<option
value={domain.name + '/'}
key={domain.name}
selected={props?.apiScope?.name?.includes(
domain.name + '/',
)}
title={domain.description}
>
{domain.name + '/'}
</option>
)
})}
</select>
</div>

<div className="api-scope-form__container__field">
<label
htmlFor="apiScope.name"
Expand All @@ -187,7 +141,6 @@ const ApiScopeCreateForm: React.FC<Props> = (props) => {
className="api-scope-form__input"
title={localization.fields['name'].helpText}
defaultValue={props.apiScope.name}
readOnly={isEditing || !domainIsTouched}
onChange={(e) => onApiScopeNameChange(e.target.value)}
placeholder={localization.fields['name'].placeholder}
onBlur={() => setApiScopeNameHintVisible(false)}
Expand Down Expand Up @@ -218,7 +171,6 @@ const ApiScopeCreateForm: React.FC<Props> = (props) => {
message={localization.fields['name'].errorMessage}
/>
</div>

<div className="api-scope-form__container__field">
<label
htmlFor="apiScope.displayName"
Expand Down Expand Up @@ -291,6 +243,41 @@ const ApiScopeCreateForm: React.FC<Props> = (props) => {
id={props.apiScope.name}
/>
</div>
<div className="api-scope-form__container__field">
<label htmlFor="domainName" className="api-scope-form__label">
{localization.fields['domainName'].label}
</label>
<select
id="apiScope.domainName"
name="apiScope.domainName"
ref={register({
required: true,
})}
placeholder={localization.fields['domainName'].placeholder}
title={localization.fields['domainName'].helpText}
>
{domains.map((domain: Domain) => {
return (
<option
value={domain.name}
key={domain.name}
selected={props.apiScope.domainName === domain.name}
>
{domain.name}
</option>
)
})}
</select>
<HelpBox
helpText={localization.fields['domainName'].helpText}
/>
<ErrorMessage
as="span"
errors={errors}
name="domainName"
message={localization.fields['domainName'].errorMessage}
/>
</div>
<div className="api-scope-form__container__field">
<label
htmlFor="apiScope.groupId"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,13 @@ const ApiScopeGroupCreateForm: React.FC<Props> = (props: Props) => {
>
{domains.map((domain: Domain) => {
return (
<option value={domain.name} key={domain.name}>
<option
value={domain.name}
key={domain.name}
selected={
props.apiScopeGroup.domainName === domain.name
}
>
{domain.name}
</option>
)
Expand Down
1 change: 1 addition & 0 deletions apps/auth-admin-web/entities/dtos/api-scope-dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,5 @@ export class ApiScopeDTO {
alsoForDelegatedUser: boolean
isAccessControlled: boolean
groupId?: string
domainName!: string
}
1 change: 1 addition & 0 deletions apps/auth-admin-web/entities/models/api-scope.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ export class ApiScope {
readonly created!: Date
readonly modified?: Date
group?: ApiScopeGroup
domainName!: string
}
6 changes: 4 additions & 2 deletions apps/auth-admin-web/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1451,8 +1451,10 @@
}
},
"fields": {
"domain": {
"selectAnItem": "Select a Domain"
"domainName": {
"label": "Domain",
"helpText": "The domain associated with this Api Scope",
"errorMessage": "Domain is required "
},
"name": {
"label": "Name",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
ApiScope,
DelegationDTO,
DelegationType,
Domain,
PersonalRepresentative,
PersonalRepresentativeRight,
PersonalRepresentativeRightType,
Expand All @@ -14,6 +15,7 @@ import {
createNationalRegistryUser,
} from '@island.is/testing/fixtures'
import { TestApp } from '@island.is/testing/nest'
import { getModelToken } from '@nestjs/sequelize'
import faker from 'faker'
import request from 'supertest'
import { setupWithAuth } from '../../../../test/setup'
Expand All @@ -30,6 +32,7 @@ import {
getScopePermission,
personalRepresentativeType,
} from '../../../../test/stubs/personalRepresentativeStubs'
import { createDomain } from '../../../../test/stubs/domain.fixture'

describe('DelegationsController', () => {
describe('Given a user is authenticated', () => {
Expand Down Expand Up @@ -300,9 +303,12 @@ describe('DelegationsController', () => {
]

beforeAll(async () => {
const domainModel = app.get<typeof Domain>(getModelToken(Domain))
const domain = await domainModel.create(createDomain())

await apiScopeModel.bulkCreate(
scopes.map(([name, enabled, _]) =>
getPRenabledApiScope(enabled, name),
getPRenabledApiScope(domain.name, enabled, name),
),
)
await prScopePermission.bulkCreate(
Expand Down
17 changes: 17 additions & 0 deletions apps/services/auth/api/test/stubs/domain.fixture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import faker from 'faker'
import { Domain } from '@island.is/auth-api-lib'
import { createNationalId } from '@island.is/testing/fixtures'

export type CreateDomain = Pick<Domain, 'name' | 'description' | 'nationalId'>

export const createDomain = ({
name,
description,
nationalId,
}: Partial<CreateDomain> = {}): CreateDomain => {
return {
name: name ?? faker.random.word(),
description: description ?? faker.lorem.sentence(),
nationalId: nationalId ?? createNationalId('company'),
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,15 @@ export const getPersonalRepresentativeRights = (
})

export const getPRenabledApiScope = (
domainName: string,
enabled = true,
name = faker.random.word(),
): CreationAttributes<ApiScope> => ({
enabled,
name,
displayName: name,
description: faker.random.words(),
domainName: domainName,
grantToPersonalRepresentatives: true,
grantToLegalGuardians: false,
grantToProcuringHolders: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const createRandomApiScope = (): ApiScopesDTO => {
automaticDelegationGrant: false,
alsoForDelegatedUser: false,
grantToPersonalRepresentatives: false,
domainName: faker.random.word(),
}
}

Expand Down
7 changes: 6 additions & 1 deletion apps/services/auth/public-api/test/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,12 @@ export const setupWithAuth = async ({

// Add scopes in the "system" to use for delegation setup
const apiScopeModel = app.get<typeof ApiScope>(getModelToken(ApiScope))
await apiScopeModel.bulkCreate(scopes.map((scope) => createApiScope(scope)))
await apiScopeModel.bulkCreate(
scopes.map((scope) => ({
...createApiScope(scope),
domainName: domain.name,
})),
)

// Add language for translations.
const languageModel = app.get<typeof Language>(getModelToken(Language))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
'use strict'

module.exports = {
async up(queryInterface, Sequelize) {
return queryInterface.sequelize.transaction(async (transaction) => {
await queryInterface.addColumn(
'api_scope',
'domain_name',
{
type: Sequelize.STRING,
allowNull: true,
references: { model: 'domain', key: 'name' },
},
{ transaction },
)

await queryInterface.sequelize.query(
`UPDATE api_scope SET domain_name = (SELECT domain_name FROM api_scope_group WHERE id = api_scope.group_id);`,
{ transaction },
)

await queryInterface.sequelize.query(
`INSERT INTO domain
(name, description, national_id, display_name, organisation_logo_key)
SELECT domain_segment, domain_segment, '', domain_segment, domain_segment
FROM (SELECT DISTINCT split_part(name, '/', 1) AS domain_segment FROM api_scope WHERE domain_name IS null) t
WHERE NOT EXISTS (SELECT 0 FROM domain WHERE name = t.domain_segment);`,
{ transaction },
)

await queryInterface.sequelize.query(
`UPDATE api_scope SET domain_name = split_part(name, '/', 1)
WHERE domain_name IS null;`,
{ transaction },
)

await queryInterface.changeColumn(
'api_scope',
'domain_name',
{
type: Sequelize.STRING,
allowNull: false,
},
{ transaction },
)

await queryInterface.addIndex('api_scope', ['domain_name'], {
transaction,
})
})
},

async down(queryInterface, Sequelize) {
return queryInterface.sequelize.transaction(async (transaction) => {
await queryInterface.removeColumn('api_scope', 'domain_name', {
transaction,
})
})
},
}
6 changes: 6 additions & 0 deletions libs/auth-api-lib/src/lib/resources/dto/api-scopes.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ export class ApiScopesDTO {
})
readonly description!: string

@IsString()
@ApiProperty({
example: '@island.is',
})
readonly domainName!: string

@IsInt()
@Min(0)
@Max(999)
Expand Down
11 changes: 11 additions & 0 deletions libs/auth-api-lib/src/lib/resources/models/api-scope.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ import { ApiScopesDTO } from '../dto/api-scopes.dto'
import { DelegationScope } from '../../delegations/models/delegation-scope.model'
import { PersonalRepresentativeScopePermission } from '../../personal-representative/models/personal-representative-scope-permission.model'
import { Optional } from 'sequelize/types'
import { Domain } from './domain.model'

interface ModelAttributes {
name: string
enabled: boolean
displayName: string
description: string
domainName: string
groupId?: string | null
showInDiscoveryDocument: boolean
grantToLegalGuardians: boolean
Expand Down Expand Up @@ -94,6 +96,14 @@ export class ApiScope extends Model<ModelAttributes, CreationAttributes> {
@ApiProperty()
description!: string

@Column({
type: DataType.STRING,
allowNull: false,
})
@ApiProperty({ example: '@island.is' })
@ForeignKey(() => Domain)
domainName!: string

@Column({
type: DataType.NUMBER,
allowNull: false,
Expand Down Expand Up @@ -242,6 +252,7 @@ export class ApiScope extends Model<ModelAttributes, CreationAttributes> {
alsoForDelegatedUser: this.alsoForDelegatedUser,
required: this.required,
emphasize: this.emphasize,
domainName: this.domainName,
}
}
}
5 changes: 5 additions & 0 deletions libs/auth-api-lib/src/lib/resources/models/domain.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
HasMany,
} from 'sequelize-typescript'
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'
import { ApiScope } from './api-scope.model'
import { ApiScopeGroup } from './api-scope-group.model'

@Table({
Expand Down Expand Up @@ -73,4 +74,8 @@ export class Domain extends Model {
@HasMany(() => ApiScopeGroup)
@ApiPropertyOptional()
groups?: ApiScopeGroup[]

@HasMany(() => ApiScope)
@ApiPropertyOptional()
scopes?: ApiScope[]
}
Loading

0 comments on commit 2a0c1be

Please sign in to comment.