diff --git a/docs/site/migration/auth/example.md b/docs/site/migration/auth/example.md index ade81665be86..7969887b633e 100644 --- a/docs/site/migration/auth/example.md +++ b/docs/site/migration/auth/example.md @@ -297,93 +297,95 @@ To make the tutorial concise, the code details is omitted here. You can find the ACLs and how the endpoints are decorated in file [src/controller/project.controller.ts](https://github.com/strongloop/loopback-next/tree/master/examples/access-control-migration/src/controllers/project.controller.ts) -Next, we write the authorizer that calls casbin enforcers to make the decision +Next, we write the authorizer that calls casbin enforcers to make the decision. 1. Create an authorizer that retrieves the authorization metadata from context, then execute casbin enforcers to make decision. -_If you are not familiar with the concept authorizer, you can learn it in the -document -[Programming Access Policies](../../Loopback-component-authorization#programming-access-policies)_ + _If you are not familiar with the concept authorizer, you can learn it in the + document + [Programming Access Policies](../../Loopback-component-authorization#programming-access-policies)_ -The complete authorizer file can be found in -[components/casbin-authorization/services/casbin.authorizer.ts](https://github.com/strongloop/loopback-next/blob/master/examples/access-control-migration/src/components/casbin-authorization/services/casbin.authorizer.ts). -It retrieves the three required fields from the authorization context: subject -from `principal`, `resource` as object, and action, to invoke casbin enforcers -and make decision. + The complete authorizer file can be found in + [components/casbin-authorization/services/casbin.authorizer.ts](https://github.com/strongloop/loopback-next/blob/master/examples/access-control-migration/src/components/casbin-authorization/services/casbin.authorizer.ts). + It retrieves the three required fields from the authorization context: + subject from `principal`, `resource` as object, and action, to invoke casbin + enforcers and make decision. 2. Create a voter for instance level endpoints to append the project id to the resource name -Class level means operations applied to all projects like `/projects/*`, whereas -instance level operation applies to a certain project like `/project{id}/*`. For -instance level endpoints, the resource name should include the resource's `id`. -Since the `id` comes from the request, the endpoint metadata cannot provide it -and therefore we define the pre-process logic in a voter to generate the -resource name as `project${id}`, then passes it to the authorizer. + Class level means operations applied to all projects like `/projects/*`, + whereas instance level operation applies to a certain project like + `/project{id}/*`. For instance level endpoints, the resource name should + include the resource's `id`. Since the `id` comes from the request, the + endpoint metadata cannot provide it and therefore we define the pre-process + logic in a voter to generate the resource name as `project${id}`, then passes + it to the authorizer. -The complete voter file can be found in file -[components/casbin-authorization/services/assign-project-instance-id.voter.ts](https://github.com/strongloop/loopback-next/blob/master/examples/access-control-migration/src/components/casbin-authorization/services/assign-project-instance-id.voter.ts) + The complete voter file can be found in file + [components/casbin-authorization/services/assign-project-instance-id.voter.ts](https://github.com/strongloop/loopback-next/blob/master/examples/access-control-migration/src/components/casbin-authorization/services/assign-project-instance-id.voter.ts) 3. Create casbin enforcers -A casbin enforcer is configured with a policy file. It compares the given data -with the policy file and returns a decision when invoked. To optimize the scope -and speed of the access check, this example splits the policies into separate -files per role. The authorizer will only invoke the enforcers for allowed -role(s). + A casbin enforcer is configured with a policy file. It compares the given + data with the policy file and returns a decision when invoked. To optimize + the scope and speed of the access check, this example splits the policies + into separate files per role. The authorizer will only invoke the enforcers + for allowed role(s). -The complete enforcer file can be found in file -[components/casbin-authorization/services/casbin.enforcers.ts](https://github.com/strongloop/loopback-next/blob/master/examples/access-control-migration/src/components/casbin-authorization/services/casbin.enforcers.ts) + The complete enforcer file can be found in file + [components/casbin-authorization/services/casbin.enforcers.ts](https://github.com/strongloop/loopback-next/blob/master/examples/access-control-migration/src/components/casbin-authorization/services/casbin.enforcers.ts) 4. Write casbin model and policies -Since the model and policy are already covered in section -[Using Casbin](#using-casbin), we will not repeat it here. The corresponding -files are defined in folder -[fixtures/casbin](https://github.com/strongloop/loopback-next/blob/master/examples/access-control-migration/fixtures/casbin). + Since the model and policy are already covered in section + [Using Casbin](#using-casbin), we will not repeat it here. The corresponding + files are defined in folder + [fixtures/casbin](https://github.com/strongloop/loopback-next/blob/master/examples/access-control-migration/fixtures/casbin). 5. Mount the casbin authorization system as a component -The casbin authorizer, voter and enforcers above are packed under component -'src/components/casbin-authorization'. You can export their bindings in a -[component file](https://github.com/strongloop/loopback-next/blob/master/examples/access-control-migration/src/components/casbin-authorization/casbin-authorization-component.ts) -and mount the component in the application constructor: - -{% include code-caption.html content="src/application.ts" %} - -```ts -// Add this line to import the component -import {CasbinAuthorizationComponent} from './components/casbin-authorization'; - -export class AccessControlApplication extends BootMixin( - ServiceMixin(RepositoryMixin(RestApplication)), -) { - constructor(options: ApplicationConfig = {}) { - // ... - // Add this line to mount the casin authorization component - this.component(CasbinAuthorizationComponent); - // ... - } -} -``` + The casbin authorizer, voter and enforcers above are packed under component + 'src/components/casbin-authorization'. You can export their bindings in a + [component file](https://github.com/strongloop/loopback-next/blob/master/examples/access-control-migration/src/components/casbin-authorization/casbin-authorization-component.ts) + and mount the component in the application constructor: + + {% include code-caption.html content="src/application.ts" %} + + ```ts + // Add this line to import the component + import {CasbinAuthorizationComponent} from './components/casbin-authorization'; + + export class AccessControlApplication extends BootMixin( + ServiceMixin(RepositoryMixin(RestApplication)), + ) { + constructor(options: ApplicationConfig = {}) { + // ... + // Add this line to mount the casin authorization component + this.component(CasbinAuthorizationComponent); + // ... + } + } + ``` 6. Casbin persistency and synchronize(2nd Phase implementation, TBD) -This will be supported at the 2nd phase of implementation. The plan is to have -model or operation hooks to update the casbin policies when new data created. It -requires a persistent storage for casbin policies (see reference in -[casbin policy persistence](https://github.com/casbin/casbin#policy-persistence)). -Here is an overview of the hooks: - -- when create a new project - - create a set of p, project\${id}\_owner, action policies - - create a set of p, project\${id}\_team, action policies -- add a new user to a team - - find the projects owned by the team owner, then create role inherit rules g, - u${id}, project${id}\_team -- add a new endpoint(operation) - - for each of its allowed roles, add p, \${role}, action policy + This will be supported at the 2nd phase of implementation. The plan is to + have model or operation hooks to update the casbin policies when new data + created. It requires a persistent storage for casbin policies (see reference + in + [casbin policy persistence](https://github.com/casbin/casbin#policy-persistence)). + Here is an overview of the hooks: + + - when create a new project + - create a set of p, project\${id}\_owner, action policies + - create a set of p, project\${id}\_team, action policies + - add a new user to a team + - find the projects owned by the team owner, then create role inherit rules + g, u${id}, project${id}\_team + - add a new endpoint(operation) + - for each of its allowed roles, add p, \${role}, action policy #### Summary @@ -446,61 +448,25 @@ Try the 'team-member' role: ### Model Creation -Create User Model +Import model `User`, `Project`, `Team` from the LB3 application by the migration +CLI: ```sh -$ lb4 model -? Model class name: User -? Please select the model base class Entity (A persisted model with an ID) -? Allow additional (free-form) properties? Yes -Model User will be created in src/models/user.model.ts - -Lets add a property to User -Enter an empty property name when done - -? Enter the property name: id -? Property type: number -? Is id the ID property? Yes -? Is id generated automatically? No -? Is it required?: Yes -? Default value [leave blank for none]: - -Lets add another property to User -Enter an empty property name when done - -? Enter the property name: username -? Property type: string -? Is it required?: Yes -? Default value [leave blank for none]: - -Lets add another property to User -Enter an empty property name when done - -? Enter the property name: email -? Property type: string -? Is it required?: Yes -? Default value [leave blank for none]: - -Lets add another property to User -Enter an empty property name when done - -? Enter the property name: - create src/models/user.model.ts - update src/models/index.ts - -Model User was created in src/models/ +$ lb4 import-lb3-models --outDir src/models ``` -Create Team Model +Choose model `User`, `Project`, `Team` for the prompt. + +For model `UserCredentials`, you need to create it by `lb4 model`: ```sh $ lb4 model -? Model class name: Team +? Model class name: UserCredentials ? Please select the model base class Entity (A persisted model with an ID) ? Allow additional (free-form) properties? Yes -Model Team1 will be created in src/models/team.model.ts +Model UserCredentials will be created in src/models/user-credentials.model.ts -Lets add a property to Team +Lets add a property to UserCredentials Enter an empty property name when done ? Enter the property name: id @@ -510,132 +476,131 @@ Enter an empty property name when done ? Is it required?: Yes ? Default value [leave blank for none]: -Lets add another property to Team +Lets add another property to UserCredentials Enter an empty property name when done -? Enter the property name: ownerId -? Property type: number +? Enter the property name: password +? Property type: string ? Is it required?: Yes ? Default value [leave blank for none]: -Lets add another property to Team +Lets add another property to UserCredentials Enter an empty property name when done -? Enter the property name: memberIds -? Property type: array -? Type of array items: number +? Enter the property name: userId +? Property type: number ? Is it required?: Yes ? Default value [leave blank for none]: -Lets add another property to Team +Lets add another property to UserCredentials Enter an empty property name when done ? Enter the property name: - create src/models/team.model.ts + create src/models/user-credentials.model.ts update src/models/index.ts -Model Team was created in src/models/ +Model UserCredentials was created in src/models/ ``` -Create Project Model +Considering the difference between the original application and the migrated +one, you need to adjust the properties a bit for the three migrated models. -```sh -$ lb4 model -? Model class name: Project -? Please select the model base class Entity (A persisted model with an ID) -? Allow additional (free-form) properties? Yes -Model Project1 will be created in src/models/project.model.ts +- For `Project`, open file `src/models/project.model.ts` -Lets add a property to Project -Enter an empty property name when done + - decorate `ownerId` with `@belongsTo(() => User)` and remove the generated + `@property` decorator. + - `balance` should be a required property: change it from `balance?` to + `balance` -? Enter the property name: id -? Property type: number -? Is id the ID property? Yes -? Is id generated automatically? No -? Is it required?: Yes -? Default value [leave blank for none]: +- For `Team`, open file `src/models/team.model.ts` -Lets add another property to Project -Enter an empty property name when done - -? Enter the property name: name -? Property type: string -? Is it required?: Yes -? Default value [leave blank for none]: - -Lets add another property to Project -Enter an empty property name when done - -? Enter the property name: balance -? Property type: number -? Is it required?: Yes -? Default value [leave blank for none]: + - replace `memberId` with `memberIds` as an array: + ```ts + @property({ + type: 'array', + itemType: 'number', + required: true, + }) + memberIds: number[]; + ``` -Lets add another property to Project -Enter an empty property name when done +- For `User`, open file `src/models/user.model.ts` -? Enter the property name: - create src/models/project.model.ts - update src/models/index.ts + - remove `password` because we have a `UserCredential` model created to + separate it from `User` -Model Project was created in src/models/ -``` +- For all the three models above, allow generating their `id` field: + ```ts + @property({ + type: 'number', + id: 1, + // change it from `true` to `false` + generated: false, + updateOnly: true, + }) + // change it from optional `id?` to required `id` + id: number; + ``` -Create UserCredentials +Create corresponding repositories: ```sh -$ lb4 model -? Model class name: UserCredentials -? Please select the model base class Entity (A persisted model with an ID) -? Allow additional (free-form) properties? Yes -Model UserCredentials will be created in src/models/user-credentials.model.ts - -Lets add a property to UserCredentials -Enter an empty property name when done - -? Enter the property name: id -? Property type: number -? Is id the ID property? Yes -? Is id generated automatically? No -? Is it required?: Yes -? Default value [leave blank for none]: - -Lets add another property to UserCredentials -Enter an empty property name when done +$ lb4 repository +? Please select the datasource DbDatasource +? Select the model(s) you want to generate a repository Project, Team, UserCredentials, User +? Please select the repository base class DefaultCrudRepository (Legacy juggler bridge) +``` -? Enter the property name: password -? Property type: string -? Is it required?: Yes -? Default value [leave blank for none]: +Create relations: -Lets add another property to UserCredentials -Enter an empty property name when done +- `Project` belongsTo `User` -? Enter the property name: userId -? Property type: number -? Is it required?: Yes -? Default value [leave blank for none]: +```sh +lb4 relation +? Please select the relation type belongsTo +? Please select source model Project +? Please select target model User +? Foreign key name to define on the source model ownerId +? Relation name owner +? Allow Project queries to include data from related User instances? Yes +``` -Lets add another property to UserCredentials -Enter an empty property name when done +{% include tip.html content=" +Please delete the generated controller file `src/controllers/project-user.controller.ts` to keep the exposed endpoints clean. +" %} -? Enter the property name: - create src/models/user-credentials.model.ts - update src/models/index.ts +- `User` hasOne `UserCredentials` -Model UserCredentials was created in src/models/ +```sh +lb4 relation +? Please select the relation type hasOne +? Please select source model User +? Please select target model UserCredentials +? Foreign key name to define on the target model userId +? Source property name for the relation getter (will be the relation name) userCredentials +? Allow User queries to include data from related UserCredentials instances? Yes ``` -Create corresponding repositories +{% include tip.html content=" +Please delete the generated controller file `src/controllers/user-user-credentials.controller.ts` to keep the exposed endpoints clean. +" %} + +- `User` hasMany `Team` ```sh -$ lb4 repository -? Please select the datasource DbDatasource -? Select the model(s) you want to generate a repository Project, Team, UserCredentials, User -? Please select the repository base class DefaultCrudRepository (Legacy juggler bridge) +lb4 relation +? Please select the relation type hasMany +? Please select source model User +? Please select target model Team +? Foreign key name to define on the target model ownerId +? Source property name for the relation getter (will be the relation name) teams +? Allow User queries to include data from related Team instances? Yes ``` +{% include tip.html content=" +Please delete the generated controller file `src/controllers/user-team.controller.ts` to keep the exposed endpoints clean. +" %} + ### User Controller Creation ```sh diff --git a/examples/access-control-migration/src/models/project.model.ts b/examples/access-control-migration/src/models/project.model.ts index 5540d296938a..20e416e1c2f3 100644 --- a/examples/access-control-migration/src/models/project.model.ts +++ b/examples/access-control-migration/src/models/project.model.ts @@ -3,32 +3,32 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {Entity, model, property, belongsTo} from '@loopback/repository'; +import {belongsTo, Entity, model, property} from '@loopback/repository'; import {User} from './user.model'; @model({settings: {strict: false}}) export class Project extends Entity { @property({ type: 'number', - id: true, + id: 1, generated: false, + updateOnly: true, }) id: number; @property({ type: 'string', - required: true, }) - name: string; + name?: string; @property({ type: 'number', - required: true, }) balance: number; @belongsTo(() => User) ownerId: number; + // Define well-known properties here // Indexer property to allow additional data diff --git a/examples/access-control-migration/src/models/team.model.ts b/examples/access-control-migration/src/models/team.model.ts index f78f9070bd07..56485d8a4c6a 100644 --- a/examples/access-control-migration/src/models/team.model.ts +++ b/examples/access-control-migration/src/models/team.model.ts @@ -9,8 +9,9 @@ import {Entity, model, property} from '@loopback/repository'; export class Team extends Entity { @property({ type: 'number', - id: true, + id: 1, generated: false, + updateOnly: true, }) id: number; @@ -20,9 +21,6 @@ export class Team extends Entity { }) ownerId: number; - // REFACTOR - // The members should be specified by relation - // instead of using an array of Ids @property({ type: 'array', itemType: 'number', diff --git a/examples/access-control-migration/src/models/user.model.ts b/examples/access-control-migration/src/models/user.model.ts index d19c076401d9..e9e671e6102f 100644 --- a/examples/access-control-migration/src/models/user.model.ts +++ b/examples/access-control-migration/src/models/user.model.ts @@ -3,36 +3,59 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {Entity, model, property, hasMany, hasOne} from '@loopback/repository'; +import {Entity, hasMany, hasOne, model, property} from '@loopback/repository'; import {Team} from './team.model'; import {UserCredentials} from './user-credentials.model'; -@model({settings: {strict: false}}) +@model({ + settings: { + strict: false, + }, +}) export class User extends Entity { + // must keep it @property({ type: 'number', - id: true, + id: 1, generated: false, + updateOnly: true, }) id: number; @property({ type: 'string', - required: true, }) - username: string; + realm?: string; + + // must keep it + @property({ + type: 'string', + }) + username?: string; + // must keep it @property({ type: 'string', required: true, }) email: string; - @hasMany(() => Team, {keyTo: 'ownerId'}) - teams: Team[]; + @property({ + type: 'boolean', + }) + emailVerified?: boolean; + + @property({ + type: 'string', + }) + verificationToken?: string; @hasOne(() => UserCredentials) userCredentials: UserCredentials; + + @hasMany(() => Team, {keyTo: 'ownerId'}) + teams: Team[]; + // Define well-known properties here // Indexer property to allow additional data diff --git a/examples/access-control-migration/src/repositories/project.repository.ts b/examples/access-control-migration/src/repositories/project.repository.ts index ad80ec6f15ca..bee7568feeb4 100644 --- a/examples/access-control-migration/src/repositories/project.repository.ts +++ b/examples/access-control-migration/src/repositories/project.repository.ts @@ -3,14 +3,14 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT +import {Getter, inject} from '@loopback/core'; import { + BelongsToAccessor, DefaultCrudRepository, repository, - BelongsToAccessor, } from '@loopback/repository'; -import {Project, ProjectRelations, User} from '../models'; import {DbDataSource} from '../datasources'; -import {inject, Getter} from '@loopback/core'; +import {Project, ProjectRelations, User} from '../models'; import {UserRepository} from './user.repository'; export class ProjectRepository extends DefaultCrudRepository< @@ -18,7 +18,7 @@ export class ProjectRepository extends DefaultCrudRepository< typeof Project.prototype.id, ProjectRelations > { - public readonly user: BelongsToAccessor; + public readonly owner: BelongsToAccessor; constructor( @inject('datasources.db') dataSource: DbDataSource, @@ -26,7 +26,7 @@ export class ProjectRepository extends DefaultCrudRepository< protected userRepositoryGetter: Getter, ) { super(Project, dataSource); - this.user = this.createBelongsToAccessorFor('owner', userRepositoryGetter); - this.registerInclusionResolver('user', this.user.inclusionResolver); + this.owner = this.createBelongsToAccessorFor('owner', userRepositoryGetter); + this.registerInclusionResolver('owner', this.owner.inclusionResolver); } } diff --git a/examples/access-control-migration/src/repositories/user.repository.ts b/examples/access-control-migration/src/repositories/user.repository.ts index dad9dc35feb2..55582b257ffe 100644 --- a/examples/access-control-migration/src/repositories/user.repository.ts +++ b/examples/access-control-migration/src/repositories/user.repository.ts @@ -3,15 +3,15 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT +import {Getter, inject} from '@loopback/core'; import { DefaultCrudRepository, - repository, HasManyRepositoryFactory, HasOneRepositoryFactory, + repository, } from '@loopback/repository'; -import {User, UserRelations, Team, UserCredentials} from '../models'; import {DbDataSource} from '../datasources'; -import {inject, Getter} from '@loopback/core'; +import {Team, User, UserCredentials, UserRelations} from '../models'; import {TeamRepository} from './team.repository'; import {UserCredentialsRepository} from './user-credentials.repository'; @@ -20,26 +20,31 @@ export class UserRepository extends DefaultCrudRepository< typeof User.prototype.id, UserRelations > { - public readonly teams: HasManyRepositoryFactory< - Team, + public readonly userCredentials: HasOneRepositoryFactory< + UserCredentials, typeof User.prototype.id >; - public readonly userCredentials: HasOneRepositoryFactory< - UserCredentials, + public readonly teams: HasManyRepositoryFactory< + Team, typeof User.prototype.id >; constructor( @inject('datasources.db') dataSource: DbDataSource, - @repository.getter('TeamRepository') - protected teamRepositoryGetter: Getter, @repository.getter('UserCredentialsRepository') protected userCredentialsRepositoryGetter: Getter< UserCredentialsRepository >, + @repository.getter('TeamRepository') + protected teamRepositoryGetter: Getter, ) { super(User, dataSource); + this.teams = this.createHasManyRepositoryFactoryFor( + 'teams', + teamRepositoryGetter, + ); + this.registerInclusionResolver('teams', this.teams.inclusionResolver); this.userCredentials = this.createHasOneRepositoryFactoryFor( 'userCredentials', userCredentialsRepositoryGetter, @@ -48,11 +53,6 @@ export class UserRepository extends DefaultCrudRepository< 'userCredentials', this.userCredentials.inclusionResolver, ); - this.teams = this.createHasManyRepositoryFactoryFor( - 'teams', - teamRepositoryGetter, - ); - this.registerInclusionResolver('teams', this.teams.inclusionResolver); } async findCredentials(