diff --git a/crunchydb/charts/crunchy-postgres/values.yaml b/crunchydb/charts/crunchy-postgres/values.yaml index 199ea9b8..6a00d299 100644 --- a/crunchydb/charts/crunchy-postgres/values.yaml +++ b/crunchydb/charts/crunchy-postgres/values.yaml @@ -1,7 +1,7 @@ fullnameOverride: strdss-db # crunchyImage: # it's not necessary to specify an image as the images specified in the Crunchy Postgres Operator will be pulled by default -crunchyImage: artifacts.developer.gov.bc.ca/bcgov-docker-local/crunchy-postgres-gis:ubi8-15.2-3.3-0 # use this image for POSTGIS +crunchyImage: artifacts.developer.gov.bc.ca/bcgov-docker-local/crunchy-postgres-gis:ubi8-15.6-3.3-0 # use this image for POSTGIS postgresVersion: 15 postGISVersion: '3.3' # use this version of POSTGIS. both crunchyImage and this property needs to have valid values for POSTGIS to be enabled. imagePullPolicy: IfNotPresent @@ -36,7 +36,6 @@ pgBackRest: repos: schedules: full: 0 8 * * * - incremental: 0 0,4,12,16,20 * * * volume: accessModes: "ReadWriteOnce" storage: 64Mi diff --git a/crunchydb/values-prod.yaml b/crunchydb/values-prod.yaml index 86fe2831..f6bab28c 100644 --- a/crunchydb/values-prod.yaml +++ b/crunchydb/values-prod.yaml @@ -4,7 +4,7 @@ crunchy-postgres: name: ha # high availability replicas: 2 dataVolumeClaimSpec: - storage: 20Gi + storage: 25Gi storageClassName: netapp-block-standard requests: cpu: 20m diff --git a/gateway/README.md b/gateway/README.md new file mode 100644 index 00000000..04d835f5 --- /dev/null +++ b/gateway/README.md @@ -0,0 +1,81 @@ +# KONG API Service Portal Setup + +The public API is accessible at + +* DEV: https://dev.strdata.api.gov.bc.ca +* UAT: https://test.strdata.api.gov.bc.ca +* PROD: https://strdata.api.gov.bc.ca + +API access is controlled via Kong, administered via the BC Gov API Programme Services API Gateway. +**Kong configuration is not updated via Github Actions, and must be updated manually when there are changes.** + +For an overview of the API Gateway update process, see: +https://bcgov.github.io/aps-infra-platform/guides/owner-journey-v1/ + + +## Publication + +### Prerequisites +1. In the API Services Portal (https://api.gov.bc.ca/), the namespace strdata has already been created. +2. In the namespace, authorization profile has been created as follows: + * Flow: Client Credential Flow, using Client ID and Secret + * Mode: Automatic + * Client Mappers (Audience): gateway-awp + + +### Publication + + +1. Log into https://api.gov.bc.ca/ +2. Select the strdata namespace +3. Create a service account with `GatewayConfig.Publish` scope and note down the client id and client secret +4. Download the GWA CLI from https://github.com/bcgov/gwa-cli/releases +5. In command prompt run the following commands (the first command create a .env file locally, which will need to be deleted if you need to create one for the other environment): + + ```sh + gwa config set host api.gov.bc.ca + gwa config set --namespace strdata + + export SCID="<>" + export SCSC="<>" + export SURL="https://authz.apps.gov.bc.ca/auth/realms/aps/protocol/openid-connect/token" + + gwa login --client-id $SCID --client-secret $SCSC + gwa pg strdata-{env}.yaml + ``` +5. (optional for Windows GWA) In command prompt of Windows run the following commands (the first command create a .env file locally, which will need to be deleted if you need to create one for the other environment): + + ```sh + gwa config set host api.gov.bc.ca + gwa config set --namespace strdata + gwa login --client-id "<>" --client-secret "<>" + gwa pg strdata-{env}.yaml + ``` +6. Check the Gateway in the API Service Portal to make sure that the routes have been published +7. Create a dataset if it doesn't exist. + + https://bcgov.github.io/aps-infra-platform/guides/owner-journey-v1/#91-setup-your-draft-dataset + + ``` + { + "name": "strdata-dataset", + "license_title": "Open Government Licence - British Columbia", + "security_class": "PUBLIC", + "view_audience": "Public", + "download_audience": "Public", + "record_publish_date": "2024-09-11", + "notes": "Short-Term Rental Data API Services", + "title": "Short-Term Rental Data API Services", + "tags": [ + "openapi", + "standards" + ], + "organization": "ministry-of-housing", + "organizationUnit": "planning-and-land-use-management" + } + ``` + +8. Create a product if it doesn't exist. + +### Consumer Request & Approval + diff --git a/gateway/strdata-dev.yaml b/gateway/strdata-dev.yaml new file mode 100644 index 00000000..d5d04fea --- /dev/null +++ b/gateway/strdata-dev.yaml @@ -0,0 +1,56 @@ +services: +- name: strdata + host: strdss-dev-backend.b0471a-dev.svc + tags: [ns.strdata] + port: 8080 + protocol: http + retries: 0 + routes: + - name: strdata + tags: [ns.strdata] + hosts: + - dev.strdata.api.gov.bc.ca + methods: + - GET + paths: [/api/organizations/types] + strip_path: false + https_redirect_status_code: 426 + path_handling: v0 + request_buffering: true + response_buffering: true + plugins: + - name: jwt-keycloak + tags: [ns.strdata] + enabled: true + config: + allowed_iss: [https://loginproxy.gov.bc.ca/auth/realms/apigw, https://dev.loginproxy.gov.bc.ca/auth/realms/apigw, https://test.loginproxy.gov.bc.ca/auth/realms/apigw] + allowed_aud: gateway-strdata + run_on_preflight: true + iss_key_grace_period: 10 + maximum_expiration: 0 + algorithm: RS256 + claims_to_verify: + - exp + uri_param_names: + - jwt + cookie_names: [] + scope: + roles: + realm_roles: + client_roles: + anonymous: + consumer_match: true + consumer_match_claim: azp + consumer_match_claim_custom_id: true + consumer_match_ignore_not_found: false + - name: request-transformer + tags: [ns.strdata] + enabled: true + config: + http_method: + - name: kong-upstream-jwt + enabled: true + tags: [ns.strdata] + config: + header: GW-JWT + include_credential_type: false diff --git a/server/StrDss.Api/Controllers/UsersController.cs b/server/StrDss.Api/Controllers/UsersController.cs index 39a7f735..4353a528 100644 --- a/server/StrDss.Api/Controllers/UsersController.cs +++ b/server/StrDss.Api/Controllers/UsersController.cs @@ -150,5 +150,19 @@ public async Task GetBceidUserInfo() var userinfo = await _userService.GetBceidUserInfo(); return Ok(userinfo); } + + [ApiAuthorize(Permissions.UserWrite)] + [HttpPost("aps", Name = "CreateApsUser")] + public async Task CreateApsUser(ApsUserCreateDto dto) + { + var errors = await _userService.CreateApsUserAsync(dto); + + if (errors.Count > 0) + { + return ValidationUtils.GetValidationErrorResult(errors, ControllerContext); + } + + return Ok(); + } } } diff --git a/server/StrDss.Data/Mappings/ModelToEntityProfile.cs b/server/StrDss.Data/Mappings/ModelToEntityProfile.cs index b52f2f29..ec9c463d 100644 --- a/server/StrDss.Data/Mappings/ModelToEntityProfile.cs +++ b/server/StrDss.Data/Mappings/ModelToEntityProfile.cs @@ -11,6 +11,7 @@ public class ModelToEntityProfile : Profile public ModelToEntityProfile() { CreateMap(); + CreateMap(); CreateMap(); CreateMap(); CreateMap(); diff --git a/server/StrDss.Data/Repositories/UserRepository.cs b/server/StrDss.Data/Repositories/UserRepository.cs index 7b07d34f..c7eb7087 100644 --- a/server/StrDss.Data/Repositories/UserRepository.cs +++ b/server/StrDss.Data/Repositories/UserRepository.cs @@ -24,6 +24,7 @@ public interface IUserRepository Task> GetAccessRequestStatuses(); Task AcceptTermsConditions(); Task UpdateUserNamesAsync(long userId, string firstName, string lastName); + Task CreateApsUserAsync(ApsUserCreateDto dto); } public class UserRepository : RepositoryBase, IUserRepository { @@ -207,5 +208,20 @@ public async Task UpdateUserNamesAsync(long userId, string firstName, string las entity.FamilyNm = lastName; entity.GivenNm = firstName; } + + public async Task CreateApsUserAsync(ApsUserCreateDto dto) + { + var userEntity = _mapper.Map(dto); + + var roleCds = dto.RoleCds.Distinct(); + + foreach (var roleCd in roleCds) + { + userEntity.DssUserRoleAssignments + .Add(new DssUserRoleAssignment { UserRoleCd = roleCd }); + } + + await _dbContext.AddAsync(userEntity); + } } } diff --git a/server/StrDss.Model/UserDtos/ApsUserCreateDto.cs b/server/StrDss.Model/UserDtos/ApsUserCreateDto.cs new file mode 100644 index 00000000..d0fd8399 --- /dev/null +++ b/server/StrDss.Model/UserDtos/ApsUserCreateDto.cs @@ -0,0 +1,45 @@ +using System.Text.Json.Serialization; + +namespace StrDss.Model.UserDtos +{ + public class ApsUserCreateDto : IOrgRoles + { + [JsonIgnore] + public Guid UserGuid { get; set; } + + public string DisplayNm { get; set; } = ""; + + [JsonIgnore] + public string IdentityProviderNm { get; set; } = "aps"; + + public bool IsEnabled { get; set; } = true; + + [JsonIgnore] + public string AccessRequestStatusCd { get; set; } = "Approved"; + + [JsonIgnore] + public DateTime? AccessRequestDtm { get; set; } = DateTime.UtcNow; + + [JsonIgnore] + public string? AccessRequestJustificationTxt { get; set; } = ""; + + [JsonIgnore] + public string? GivenNm { get; set; } = ""; + + [JsonIgnore] + public string? FamilyNm { get; set; } = ""; + + [JsonIgnore] + public string? EmailAddressDsc { get; set; } = ""; + + [JsonIgnore] + public string? BusinessNm { get; set; } = ""; + + [JsonIgnore] + public DateTime? TermsAcceptanceDtm { get; set; } = DateTime.UtcNow; + + public long RepresentedByOrganizationId { get; set; } + + public List RoleCds { get; set; } = new List(); + } +} diff --git a/server/StrDss.Model/UserDtos/IOrgRoles.cs b/server/StrDss.Model/UserDtos/IOrgRoles.cs new file mode 100644 index 00000000..362df733 --- /dev/null +++ b/server/StrDss.Model/UserDtos/IOrgRoles.cs @@ -0,0 +1,8 @@ +namespace StrDss.Model.UserDtos +{ + public interface IOrgRoles + { + public long RepresentedByOrganizationId { get; set; } + public List RoleCds { get; set; } + } +} diff --git a/server/StrDss.Model/UserDtos/UserUpdateDto.cs b/server/StrDss.Model/UserDtos/UserUpdateDto.cs index 0fae71ac..0807321d 100644 --- a/server/StrDss.Model/UserDtos/UserUpdateDto.cs +++ b/server/StrDss.Model/UserDtos/UserUpdateDto.cs @@ -1,6 +1,6 @@ namespace StrDss.Model.UserDtos { - public class UserUpdateDto + public class UserUpdateDto : IOrgRoles { public long UserIdentityId { get; set; } public long RepresentedByOrganizationId { get; set; } diff --git a/server/StrDss.Service/UserService.cs b/server/StrDss.Service/UserService.cs index c7ac6fd3..4392bdf5 100644 --- a/server/StrDss.Service/UserService.cs +++ b/server/StrDss.Service/UserService.cs @@ -27,6 +27,7 @@ public interface IUserService Task GetUserByIdAsync(long userId); Task>> UpdateUserAsync(UserUpdateDto dto); Task GetBceidUserInfo(); + Task>> CreateApsUserAsync(ApsUserCreateDto dto); } public class UserService : ServiceBase, IUserService { @@ -480,6 +481,13 @@ public async Task>> ValidateUserUpdateDto(UserUp return errors; } + await ValidateOrgAndRoles(dto, errors); + + return errors; + } + + private async Task ValidateOrgAndRoles(IOrgRoles dto, Dictionary> errors) + { var org = await _orgRepo.GetOrganizationByIdAsync(dto.RepresentedByOrganizationId); if (org == null) { @@ -494,8 +502,6 @@ public async Task>> ValidateUserUpdateDto(UserUp errors.AddItem("roleCd", $"Role ({roleCd}) doesn't exist"); } } - - return errors; } public async Task GetBceidUserInfo() @@ -516,5 +522,25 @@ public async Task>> ValidateUserUpdateDto(UserUp return null; } + + public async Task>> CreateApsUserAsync(ApsUserCreateDto dto) + { + var errors = new Dictionary>(); + + if (string.IsNullOrEmpty(dto.DisplayNm)) + { + errors.AddItem("client_id", "Client ID is mandatory."); + } + + await ValidateOrgAndRoles(dto, errors); + + if (errors.Count > 0) return errors; + + await _userRepo.CreateApsUserAsync(dto); + + _unitOfWork.Commit(); + + return errors; + } } }