Skip to content

Commit

Permalink
Merge pull request #53 from AustrianDataLAB/fix/auth-header-swa
Browse files Browse the repository at this point in the history
Support static web app auth header
  • Loading branch information
rettetdemdativ authored Jun 21, 2024
2 parents a36a12d + b004e95 commit 686c1b3
Show file tree
Hide file tree
Showing 13 changed files with 173 additions and 144 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/backend_ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ jobs:
- name: Install Azure Functions Core Tools
run: |
npm i -g azure-functions-core-tools@4 --unsafe-perm true
npm i -g azure-functions-core-tools@4.0.5801 --unsafe-perm true
func --version
- name: Install python dependencies
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/tf_apply.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ jobs:
uses: pietrobolcato/install-azure-cli-action@main

- name: Setup azure core tools
run: npm i -g azure-functions-core-tools@4 --unsafe-perm true
run: npm i -g azure-functions-core-tools@4.0.5801 --unsafe-perm true

- name: Setup Terraform
uses: hashicorp/setup-terraform@v2
Expand Down
18 changes: 4 additions & 14 deletions backend/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,6 @@ import (
"github.com/AustrianDataLAB/GeWoScout/backend/cosmos"
)

const (
XMSClientPrincipalHeaderName = "X-MS-CLIENT-PRINCIPAL"
)

type Handler struct {
cosmosOnce sync.Once
// Do NOT access directly
Expand Down Expand Up @@ -261,6 +257,7 @@ func (h *Handler) GetListingById(w http.ResponseWriter, r *http.Request) {
// @Accept json
// @Produce json
// @Param city path string true "The city the preferences relate to"
// @Param preferences body models.NotificationSettings true "The user's new preferences"
// @Success 200 {object} models.NotificationSettings "Successfully updates notification settings"
// @Failure 404 {object} models.Error "Notification settings could not be updated"
// @Failure 400 {object} models.Error "Bad request"
Expand All @@ -277,7 +274,7 @@ func (h *Handler) UpdateUserPrefs(w http.ResponseWriter, r *http.Request) {
return
}

clientId, _, err := GetClientPrincipalData(&req)
clientId, email, err := GetClientPrincipalData(&req)
if err != nil {
render.JSON(w, r, models.NewHttpInvokeResponse(
http.StatusUnauthorized,
Expand Down Expand Up @@ -308,24 +305,17 @@ func (h *Handler) UpdateUserPrefs(w http.ResponseWriter, r *http.Request) {
return
}

if ns.City == nil || *ns.City == "" {
render.JSON(w, r, models.NewHttpInvokeResponse(
http.StatusBadRequest,
models.Error{Message: "Missing city in preferences"},
[]string{"Missing city in preferences"},
))
return
}

city := req.Data.Req.Params["city"]
cLower := strings.ToLower(city)

ns.PartitionKey = cLower
ns.Id = clientId
ns.Email = email
ns.City = &cLower

ud.PartitionKey = clientId
ud.Id = cLower
ud.Email = email
ud.City = &cLower

ir := models.NewHttpInvokeResponse(http.StatusOK, ud, nil)
Expand Down
63 changes: 35 additions & 28 deletions backend/api/scraper_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,28 +25,31 @@ func (h *Handler) HandleScraperResult(w http.ResponseWriter, r *http.Request) {
injectedData := models.QueueBindingInput{}
dec := json.NewDecoder(r.Body)
if err := dec.Decode(&injectedData); err != nil {
render.Status(r, http.StatusInternalServerError)
render.JSON(w, r, models.InvokeResponse{
Logs: []string{fmt.Sprintf("ScraperResultHandler | Failed to read invoke request body: %s", err.Error())},
})
render.JSON(w, r, models.NewHttpInvokeResponse(
http.StatusInternalServerError,
nil,
[]string{fmt.Sprintf("ScraperResultHandler | Failed to read invoke request body: %s", err.Error())},
))
return
}

msgId, err := strconv.Unquote(injectedData.Metadata.Id)
if err != nil {
render.Status(r, http.StatusInternalServerError)
render.JSON(w, r, models.InvokeResponse{
Logs: []string{fmt.Sprintf("ScraperResultHandler %s | Failed to unquote message ID: %s", injectedData.Metadata.Id, err.Error())},
})
render.JSON(w, r, models.NewHttpInvokeResponse(
http.StatusInternalServerError,
nil,
[]string{fmt.Sprintf("ScraperResultHandler %s | Failed to unquote message ID: %s", injectedData.Metadata.Id, err.Error())},
))
return
}

msgPlain, err := strconv.Unquote(injectedData.Data.Msg)
if err != nil {
render.Status(r, http.StatusInternalServerError)
render.JSON(w, r, models.InvokeResponse{
Logs: []string{fmt.Sprintf("ScraperResultHandler %s | Failed to unquote message: %s", msgId, err.Error())},
})
render.JSON(w, r, models.NewHttpInvokeResponse(
http.StatusInternalServerError,
nil,
[]string{fmt.Sprintf("ScraperResultHandler %s | Failed to unquote message: %s", msgId, err.Error())},
))
return
}

Expand All @@ -57,27 +60,31 @@ func (h *Handler) HandleScraperResult(w http.ResponseWriter, r *http.Request) {
msgLoader := gojsonschema.NewStringLoader(msgPlain)
result, err := h.ScraperResultSchema.Validate(msgLoader)
if err != nil {
render.Status(r, http.StatusUnprocessableEntity)
render.JSON(w, r, models.InvokeResponse{
Logs: []string{fmt.Sprintf("ScraperResultHandler %s | Failed to create message schema loader: %s", msgId, err.Error())},
})
render.JSON(w, r, models.NewHttpInvokeResponse(
http.StatusUnprocessableEntity,
nil,
[]string{fmt.Sprintf("ScraperResultHandler %s | Failed to create message schema loader: %s", msgId, err.Error())},
))
return
}

if !result.Valid() {
render.JSON(w, r, models.InvokeResponse{
Logs: []string{fmt.Sprintf("ScraperResultHandler %s | Message validation failed: %s", msgId, result.Errors())},
})
render.JSON(w, r, models.NewHttpInvokeResponse(
http.StatusInternalServerError,
nil,
[]string{fmt.Sprintf("ScraperResultHandler %s | Message validation failed: %s", msgId, result.Errors())},
))
return
}

scraperResult := models.ScraperResultList{}
err = json.Unmarshal([]byte(msgPlain), &scraperResult)
if err != nil {
render.Status(r, http.StatusUnprocessableEntity)
render.JSON(w, r, models.InvokeResponse{
Logs: []string{fmt.Sprintf("ScraperResultHandler %s | Failed to unmarshal message: %s", msgId, err.Error())},
})
render.JSON(w, r, models.NewHttpInvokeResponse(
http.StatusUnprocessableEntity,
nil,
[]string{fmt.Sprintf("ScraperResultHandler %s | Failed to unmarshal message: %s", msgId, err.Error())},
))
return
}

Expand All @@ -96,10 +103,11 @@ func (h *Handler) HandleScraperResult(w http.ResponseWriter, r *http.Request) {

nonExIds, err := cosmos.GetNonExistingIds(ctx, h.GetListingsByCityContainerClient(), idsByPk)
if err != nil {
render.Status(r, http.StatusInternalServerError)
render.JSON(w, r, models.InvokeResponse{
Logs: []string{fmt.Sprintf("ScraperResultHandler %s | Failed to get non-existing IDs: %s", msgId, err.Error())},
})
render.JSON(w, r, models.NewHttpInvokeResponse(
http.StatusInternalServerError,
nil,
[]string{fmt.Sprintf("ScraperResultHandler %s | Failed to get non-existing IDs: %s", msgId, err.Error())},
))
return
}

Expand Down Expand Up @@ -172,7 +180,6 @@ func (h *Handler) HandleScraperResult(w http.ResponseWriter, r *http.Request) {
},
}
render.JSON(w, r, invokeResponse)

}

func mapListing(scraperResult models.ScraperResultListing, scraperId string, ts time.Time) *models.Listing {
Expand Down
41 changes: 26 additions & 15 deletions backend/api/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,38 @@ import (
// id and an email address of the user.
func GetClientPrincipalData[Q any](ir *models.InvokeRequest[Q]) (string, string, error) {
principalIds, ok1 := ir.Data.Req.Headers[models.CLIENT_PRINCIPAL_ID_KEY]
principals, ok2 := ir.Data.Req.Headers[models.CLIENT_PRINCIPAL_KEY]
if !ok1 || !ok2 || !ir.Data.Req.Identities[0].IsAuthenticated {
return "", "", errors.New("user not authenticated")
}

clientId := principalIds[0]
principalB64 := principals[0]
var email string

pDec, _ := base64.StdEncoding.DecodeString(principalB64)
ok2 := false
var names, principals []string
// SWA header (StaticWebAppsAuthCookie)
if names, ok2 = ir.Data.Req.Headers[models.CLIENT_PRINCIPAL_NAME_KEY]; ok2 {
email = names[0]
} else if principals, ok2 = ir.Data.Req.Headers[models.CLIENT_PRINCIPAL_KEY]; ok2 {
// App service header
principalB64 := principals[0]

up := models.UserPrincipal{}
if err := json.Unmarshal(pDec, &up); err != nil {
return "", "", errors.New("failed to read user principal")
}
pDec, _ := base64.StdEncoding.DecodeString(principalB64)

var email string
for _, c := range up.Claims {
if c.Typ == "preferred_username" {
email = c.Val
up := models.UserPrincipal{}
if err := json.Unmarshal(pDec, &up); err != nil {
return "", "", errors.New("failed to read user principal")
}

for _, c := range up.Claims {
if c.Typ == "preferred_username" {
email = c.Val
}
}
}

if !ok1 || !ok2 || !ir.Data.Req.Identities[0].IsAuthenticated {
return "", "", errors.New("user not authenticated")
}

clientId := principalIds[0]

if email == "" {
return "", "", errors.New("failed to read user email")
}
Expand Down
34 changes: 24 additions & 10 deletions backend/cosmos/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,16 +96,22 @@ func GetListingsQueryItemsPager(

for field, mapping := range fieldMappings {
if !reflect.ValueOf(mapping.value).IsNil() {
if field == "minHwgEnergyClass" || field == "minFgeeEnergyClass" {
ecStr, _ := (mapping.value).(*models.EnergyClass)
if *ecStr != models.EnergyClassG {
addQueryParam(&sb, &queryParams, "@"+field, mapping.condition, models.GetEnergyClasses()[:ecStr.GetIndex()+1])
switch mapping.value.(type) {
case *float32:
v := mapping.value.(*float32)
addQueryParam(&sb, &queryParams, "@"+field, mapping.condition, *v)
default:
if field == "minHwgEnergyClass" || field == "minFgeeEnergyClass" {
ecStr, _ := (mapping.value).(*models.EnergyClass)
if *ecStr != models.EnergyClassG {
addQueryParam(&sb, &queryParams, "@"+field, mapping.condition, models.GetEnergyClasses()[:ecStr.GetIndex()+1])
}
} else if field == "postalCodes" {
postalCodeStr := mapping.value.(*string)
addQueryParam(&sb, &queryParams, "@"+field, mapping.condition, strings.Split(*postalCodeStr, ","))
} else {
addQueryParam(&sb, &queryParams, "@"+field, mapping.condition, mapping.value)
}
} else if field == "postalCodes" {
postalCodeStr := mapping.value.(*string)
addQueryParam(&sb, &queryParams, "@"+field, mapping.condition, strings.Split(*postalCodeStr, ","))
} else {
addQueryParam(&sb, &queryParams, "@"+field, mapping.condition, mapping.value)
}
}
}
Expand Down Expand Up @@ -314,7 +320,7 @@ func GetUsersMatchingWithListing(ctx context.Context, container *azcosmos.Contai
if !ok {
return nil, fmt.Errorf("value of %s has incorrect format", field)
}
if field == "hwgEnergyClass" || field == "fgeeEnergyClass" {
if field == "hwgEnergyClass" || field == "fgeeEnergyClass" && ecStr != nil {
ecClass := models.EnergyClass(*ecStr)
addQueryParam(&sb, &queryParams, "@"+field, mapping.condition, models.GetEnergyClasses()[ecClass.GetIndex():])
} else {
Expand All @@ -323,10 +329,18 @@ func GetUsersMatchingWithListing(ctx context.Context, container *azcosmos.Contai
}
case int:
addQueryParam(&sb, &queryParams, "@"+field, mapping.condition, mapping.value)
case float32:
v := mapping.value.(float32)
addQueryParam(&sb, &queryParams, "@"+field, mapping.condition, v)
case *int:
if mapping.value != nil {
addQueryParam(&sb, &queryParams, "@"+field, mapping.condition, mapping.value)
}
case *float32:
if mapping.value != nil {
v := mapping.value.(*float32)
addQueryParam(&sb, &queryParams, "@"+field, mapping.condition, *v)
}
default:
continue
}
Expand Down
5 changes: 3 additions & 2 deletions backend/models/azfunc.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import (
)

const (
CLIENT_PRINCIPAL_KEY = "X-MS-CLIENT-PRINCIPAL"
CLIENT_PRINCIPAL_ID_KEY = "X-MS-CLIENT-PRINCIPAL-ID"
CLIENT_PRINCIPAL_KEY = "X-MS-CLIENT-PRINCIPAL"
CLIENT_PRINCIPAL_ID_KEY = "X-MS-CLIENT-PRINCIPAL-ID"
CLIENT_PRINCIPAL_NAME_KEY = "X-MS-CLIENT-PRINCIPAL-NAME"
)

type InvokeRequest[Q any] struct {
Expand Down
8 changes: 7 additions & 1 deletion backend/models/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package models
import (
"encoding/json"
"io"
"reflect"
"time"

"github.com/go-playground/validator/v10"
Expand Down Expand Up @@ -39,7 +40,12 @@ func enumFieldValidator[T StringEnum](fl validator.FieldLevel) bool {
func gtFieldIgnoreNilValidator(fl validator.FieldLevel) bool {
otherField := fl.Parent().FieldByName(fl.Param())
if !otherField.IsNil() {
return otherField.Elem().Int() <= fl.Field().Int()
switch fl.Field().Kind() {
case reflect.Int:
return otherField.Elem().Int() <= fl.Field().Int()
case reflect.Float32:
return otherField.Elem().Float() <= fl.Field().Float()
}
}
return true
}
Expand Down
22 changes: 11 additions & 11 deletions backend/models/listings.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,23 @@ type ListingsQuery struct {
HousingCooperative *string `json:"housingCooperative" validate:"omitempty"`
ProjectId *string `json:"projectId" validate:"omitempty"`
PostalCode *string `json:"postalCode" validate:"omitempty"`
RoomCount *int `json:"roomCount,string" validate:"omitempty,gt=0"`
MinRoomCount *int `json:"minRoomCount,string" validate:"omitempty,gt=0"`
MaxRoomCount *int `json:"maxRoomCount,string" validate:"omitempty,gt=0,gtfieldcustom=MinRoomCount"`
MinSquareMeters *int `json:"minSqm,string" validate:"omitempty,gt=0"`
MaxSquareMeters *int `json:"maxSqm,string" validate:"omitempty,gt=0,gtfieldcustom=MinSquareMeters"`
RoomCount *float32 `json:"roomCount,string" validate:"omitempty,gt=0"`
MinRoomCount *float32 `json:"minRoomCount,string" validate:"omitempty,gt=0"`
MaxRoomCount *float32 `json:"maxRoomCount,string" validate:"omitempty,gt=0,gtfieldcustom=MinRoomCount"`
MinSquareMeters *float32 `json:"minSqm,string" validate:"omitempty,gt=0"`
MaxSquareMeters *float32 `json:"maxSqm,string" validate:"omitempty,gt=0,gtfieldcustom=MinSquareMeters"`
AvailableFrom *string `json:"availableFrom" validate:"omitempty,datecustom"`
MinYearBuilt *int `json:"minYearBuilt,string" validate:"omitempty,gt=1900"`
MaxYearBuilt *int `json:"maxYearBuilt,string" validate:"omitempty,gt=1900,gtfieldcustom=MinYearBuilt"`
MinHwgEnergyClass *EnergyClass `json:"minHwgEnergyClass" validate:"omitempty,energycustom"`
MinFgeeEnergyClass *EnergyClass `json:"minFgeeEnergyClass" validate:"omitempty,energycustom"`
ListingType *ListingType `json:"listingType" validate:"omitempty,listingtypecustom"`
MinRentPricePerMonth *int `json:"minRentPrice,string" validate:"omitempty,gt=0"`
MaxRentPricePerMonth *int `json:"maxRentPrice,string" validate:"omitempty,gt=0,gtfieldcustom=MinRentPricePerMonth"`
MinCooperativeShare *int `json:"minCooperativeShare,string" validate:"omitempty,gt=0"`
MaxCooperativeShare *int `json:"maxCooperativeShare,string" validate:"omitempty,gt=0,gtfieldcustom=MinCooperativeShare"`
MinSalePrice *int `json:"minSalePrice,string" validate:"omitempty,gt=0"`
MaxSalePrice *int `json:"maxSalePrice,string" validate:"omitempty,gt=0,gtfieldcustom=MinSalePrice"`
MinRentPricePerMonth *float32 `json:"minRentPrice,string" validate:"omitempty,gt=0"`
MaxRentPricePerMonth *float32 `json:"maxRentPrice,string" validate:"omitempty,gt=0,gtfieldcustom=MinRentPricePerMonth"`
MinCooperativeShare *float32 `json:"minCooperativeShare,string" validate:"omitempty,gt=0"`
MaxCooperativeShare *float32 `json:"maxCooperativeShare,string" validate:"omitempty,gt=0,gtfieldcustom=MinCooperativeShare"`
MinSalePrice *float32 `json:"minSalePrice,string" validate:"omitempty,gt=0"`
MaxSalePrice *float32 `json:"maxSalePrice,string" validate:"omitempty,gt=0,gtfieldcustom=MinSalePrice"`
ContinuationToken *string `json:"continuationToken"`
PageSize *int `json:"pageSize,string" validate:"omitempty,gt=0,lte=30"`
SortBy *string
Expand Down
Loading

0 comments on commit 686c1b3

Please sign in to comment.