diff --git a/Makefile b/Makefile index 85b37e057..1802a967d 100644 --- a/Makefile +++ b/Makefile @@ -228,6 +228,7 @@ deploy-on-openshift: oc -p MIGRATION_PLANNER_URL=http://planner-agent-$${openshift_project}.apps.$${openshift_base_url}$(SERVICE_API_PATH) \ -p MIGRATION_PLANNER_UI_URL=http://planner-ui-$${openshift_project}.apps.$${openshift_base_url} \ -p MIGRATION_PLANNER_IMAGE_URL=http://planner-image-$${openshift_project}.apps.$${openshift_base_url}$(SERVICE_API_PATH) \ + -p MIGRATION_PLANNER_ADMIN_GROUP_FILE=$(MIGRATION_PLANNER_ADMIN_GROUP_FILE) \ | oc apply -f -; \ oc expose service migration-planner-agent --name planner-agent; \ oc expose service migration-planner-image --name planner-image; \ @@ -276,6 +277,7 @@ deploy-on-kind: oc -p PERSISTENT_DISK_DEVICE=$(PERSISTENT_DISK_DEVICE) \ -p INSECURE_REGISTRY=$(INSECURE_REGISTRY) \ -p MIGRATION_PLANNER_AUTH=$(MIGRATION_PLANNER_AUTH) \ + -p MIGRATION_PLANNER_ADMIN_GROUP_FILE=$(MIGRATION_PLANNER_ADMIN_GROUP_FILE) \ -p RHCOS_PASSWORD=${RHCOS_PASSWORD} \ | oc apply -n "${MIGRATION_PLANNER_NAMESPACE}" -f -; \ echo "*** OpenShift Migration Advisor has been deployed successfully on Kind ***" diff --git a/api/v1alpha1/openapi.yaml b/api/v1alpha1/openapi.yaml index 6a64e3af6..502cace1d 100644 --- a/api/v1alpha1/openapi.yaml +++ b/api/v1alpha1/openapi.yaml @@ -1239,6 +1239,12 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + "403": + description: Forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/Error" "500": description: Internal error content: @@ -1275,6 +1281,12 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + "403": + description: Forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/Error" "500": description: Internal error content: @@ -1308,6 +1320,12 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + "403": + description: Forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/Error" "404": description: Not Found content: @@ -1358,6 +1376,12 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + "403": + description: Forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/Error" "404": description: Not Found content: @@ -1396,6 +1420,12 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + "403": + description: Forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/Error" "404": description: Not Found content: @@ -1435,6 +1465,12 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + "403": + description: Forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/Error" "404": description: Not Found content: @@ -1485,6 +1521,12 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + "403": + description: Forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/Error" "404": description: Not Found content: @@ -1548,6 +1590,12 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + "403": + description: Forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/Error" "404": description: Not Found content: @@ -1591,6 +1639,12 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + "403": + description: Forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/Error" "400": description: Bad Request content: diff --git a/api/v1alpha1/spec.gen.go b/api/v1alpha1/spec.gen.go index 056b7c5de..c66df77f5 100644 --- a/api/v1alpha1/spec.gen.go +++ b/api/v1alpha1/spec.gen.go @@ -18,168 +18,168 @@ import ( // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/deploy/e2e.mk b/deploy/e2e.mk index ce236c8b8..9cd123839 100644 --- a/deploy/e2e.mk +++ b/deploy/e2e.mk @@ -86,7 +86,11 @@ build_planner_iso_container: .PHONY: deploy_assisted_migration deploy_assisted_migration: oc - make deploy-on-kind MIGRATION_PLANNER_NAMESPACE=default PERSISTENT_DISK_DEVICE=/dev/vda + oc process --local -f deploy/templates/admin-group-template.yml \ + -p ADMIN_USERNAME=admin \ + -p ADMIN_EMAIL=admin@example.com \ + | oc apply -n default -f - + make deploy-on-kind MIGRATION_PLANNER_NAMESPACE=default PERSISTENT_DISK_DEVICE=/dev/vda MIGRATION_PLANNER_ADMIN_GROUP_FILE=/etc/planner/admin-group.yaml oc wait --for=condition=Ready pods --all --timeout=240s sleep 30 oc port-forward --address 0.0.0.0 service/migration-planner-agent 7443:7443 > /dev/null 2>&1 & diff --git a/deploy/templates/admin-group-template.yml b/deploy/templates/admin-group-template.yml new file mode 100644 index 000000000..fef9d6051 --- /dev/null +++ b/deploy/templates/admin-group-template.yml @@ -0,0 +1,24 @@ +--- +kind: Template +apiVersion: template.openshift.io/v1 +metadata: + name: migration-planner-admin-group +parameters: + - name: ADMIN_USERNAME + description: Username of the bootstrap admin user + required: true + - name: ADMIN_EMAIL + description: Email of the bootstrap admin user + required: true + +objects: + - apiVersion: v1 + kind: ConfigMap + metadata: + name: migration-planner-admin-group + data: + admin-group.yaml: | + name: admin + members: + - username: ${ADMIN_USERNAME} + email: ${ADMIN_EMAIL} diff --git a/deploy/templates/service-template.yml b/deploy/templates/service-template.yml index 46cd6cc92..0ffd4e90c 100644 --- a/deploy/templates/service-template.yml +++ b/deploy/templates/service-template.yml @@ -73,6 +73,9 @@ parameters: - name: MIGRATION_PLANNER_AGENT_AUTH_ENABLED description: Enable agent authentication for agent-server value: "true" + - name: MIGRATION_PLANNER_ADMIN_GROUP_FILE + description: Path to YAML file defining the bootstrap admin group + value: "" - name: MIGRATION_PLANNER_MIGRATIONS_FOLDER description: Path to the migration folder containing the sql files used to migrate the db value: "/app/migrations" @@ -635,6 +638,8 @@ objects: value: ${MIGRATION_PLANNER_JWK_URL} - name: MIGRATION_PLANNER_AGENT_AUTH_ENABLED value: ${MIGRATION_PLANNER_AGENT_AUTH_ENABLED} + - name: MIGRATION_PLANNER_ADMIN_GROUP_FILE + value: ${MIGRATION_PLANNER_ADMIN_GROUP_FILE} # DB Config values - name: MIGRATION_PLANNER_MIGRATIONS_FOLDER value: ${MIGRATION_PLANNER_MIGRATIONS_FOLDER} @@ -668,6 +673,9 @@ objects: mountPath: "/.migration-planner" - name: iso-storage mountPath: /iso + - name: admin-group + mountPath: /etc/planner + readOnly: true serviceAccountName: migration-planner volumes: - name: iso-storage @@ -678,6 +686,10 @@ objects: - name: envoy-config configMap: name: migration-planner-envoy-config + - name: admin-group + configMap: + name: migration-planner-admin-group + optional: true - name: envoy-unix-sockets emptyDir: medium: Memory diff --git a/go.mod b/go.mod index 732afea8e..9e782c517 100644 --- a/go.mod +++ b/go.mod @@ -40,6 +40,7 @@ require ( github.com/thoas/go-funk v0.9.3 github.com/xuri/excelize/v2 v2.9.1 go.uber.org/zap v1.27.0 + gopkg.in/yaml.v3 v3.0.1 gorm.io/driver/postgres v1.5.9 gorm.io/driver/sqlite v1.5.6 gorm.io/gorm v1.25.11 @@ -167,12 +168,10 @@ require ( golang.org/x/sys v0.44.0 // indirect golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6 // indirect golang.org/x/text v0.36.0 // indirect - golang.org/x/time v0.14.0 // indirect + golang.org/x/time v0.13.0 // indirect golang.org/x/tools v0.44.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/protobuf v1.36.11 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect - modernc.org/sqlite v1.39.1 // indirect ) replace ( diff --git a/go.sum b/go.sum index bb9bc87a0..b58a054c2 100644 --- a/go.sum +++ b/go.sum @@ -447,8 +447,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= -golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= -golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= +golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= @@ -491,13 +491,13 @@ k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY= k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8= -modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A= -modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I= +modernc.org/libc v1.65.0 h1:e183gLDnAp9VJh6gWKdTy0CThL9Pt7MfcR/0bgb7Y1Y= +modernc.org/libc v1.65.0/go.mod h1:7m9VzGq7APssBTydds2zBcxGREwvIGpuUBaKTXdm2Qs= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= -modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= -modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= -modernc.org/sqlite v1.39.1 h1:H+/wGFzuSCIEVCvXYVHX5RQglwhMOvtHSv+VtidL2r4= -modernc.org/sqlite v1.39.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE= +modernc.org/memory v1.10.0 h1:fzumd51yQ1DxcOxSO+S6X7+QTuVU+n8/Aj7swYjFfC4= +modernc.org/memory v1.10.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI= +modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/internal/api/client/client.gen.go b/internal/api/client/client.gen.go index a62672381..a41271cb6 100644 --- a/internal/api/client/client.gen.go +++ b/internal/api/client/client.gen.go @@ -3380,6 +3380,7 @@ type ListGroupsResponse struct { HTTPResponse *http.Response JSON200 *GroupList JSON401 *Error + JSON403 *Error JSON500 *Error } @@ -3405,6 +3406,7 @@ type CreateGroupResponse struct { JSON201 *Group JSON400 *Error JSON401 *Error + JSON403 *Error JSON500 *Error } @@ -3429,6 +3431,7 @@ type DeleteGroupResponse struct { HTTPResponse *http.Response JSON200 *Group JSON401 *Error + JSON403 *Error JSON404 *Error JSON500 *Error } @@ -3454,6 +3457,7 @@ type GetGroupResponse struct { HTTPResponse *http.Response JSON200 *Group JSON401 *Error + JSON403 *Error JSON404 *Error JSON500 *Error } @@ -3480,6 +3484,7 @@ type UpdateGroupResponse struct { JSON200 *Group JSON400 *Error JSON401 *Error + JSON403 *Error JSON404 *Error JSON500 *Error } @@ -3505,6 +3510,7 @@ type ListGroupMembersResponse struct { HTTPResponse *http.Response JSON200 *MemberList JSON401 *Error + JSON403 *Error JSON404 *Error JSON500 *Error } @@ -3531,6 +3537,7 @@ type CreateGroupMemberResponse struct { JSON201 *Member JSON400 *Error JSON401 *Error + JSON403 *Error JSON404 *Error JSON409 *Error JSON500 *Error @@ -3557,6 +3564,7 @@ type RemoveGroupMemberResponse struct { HTTPResponse *http.Response JSON400 *Error JSON401 *Error + JSON403 *Error JSON404 *Error JSON500 *Error } @@ -3583,6 +3591,7 @@ type UpdateGroupMemberResponse struct { JSON200 *Member JSON400 *Error JSON401 *Error + JSON403 *Error JSON404 *Error JSON500 *Error } @@ -5616,6 +5625,13 @@ func ParseListGroupsResponse(rsp *http.Response) (*ListGroupsResponse, error) { } response.JSON401 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON403 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { @@ -5663,6 +5679,13 @@ func ParseCreateGroupResponse(rsp *http.Response) (*CreateGroupResponse, error) } response.JSON401 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON403 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { @@ -5703,6 +5726,13 @@ func ParseDeleteGroupResponse(rsp *http.Response) (*DeleteGroupResponse, error) } response.JSON401 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON403 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { @@ -5750,6 +5780,13 @@ func ParseGetGroupResponse(rsp *http.Response) (*GetGroupResponse, error) { } response.JSON401 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON403 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { @@ -5804,6 +5841,13 @@ func ParseUpdateGroupResponse(rsp *http.Response) (*UpdateGroupResponse, error) } response.JSON401 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON403 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { @@ -5851,6 +5895,13 @@ func ParseListGroupMembersResponse(rsp *http.Response) (*ListGroupMembersRespons } response.JSON401 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON403 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { @@ -5905,6 +5956,13 @@ func ParseCreateGroupMemberResponse(rsp *http.Response) (*CreateGroupMemberRespo } response.JSON401 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON403 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { @@ -5959,6 +6017,13 @@ func ParseRemoveGroupMemberResponse(rsp *http.Response) (*RemoveGroupMemberRespo } response.JSON401 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON403 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { @@ -6013,6 +6078,13 @@ func ParseUpdateGroupMemberResponse(rsp *http.Response) (*UpdateGroupMemberRespo } response.JSON401 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON403 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { diff --git a/internal/api/server/server.gen.go b/internal/api/server/server.gen.go index f8a79b21a..2bed04987 100644 --- a/internal/api/server/server.gen.go +++ b/internal/api/server/server.gen.go @@ -2770,6 +2770,15 @@ func (response ListGroups401JSONResponse) VisitListGroupsResponse(w http.Respons return json.NewEncoder(w).Encode(response) } +type ListGroups403JSONResponse Error + +func (response ListGroups403JSONResponse) VisitListGroupsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(403) + + return json.NewEncoder(w).Encode(response) +} + type ListGroups500JSONResponse Error func (response ListGroups500JSONResponse) VisitListGroupsResponse(w http.ResponseWriter) error { @@ -2814,6 +2823,15 @@ func (response CreateGroup401JSONResponse) VisitCreateGroupResponse(w http.Respo return json.NewEncoder(w).Encode(response) } +type CreateGroup403JSONResponse Error + +func (response CreateGroup403JSONResponse) VisitCreateGroupResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(403) + + return json.NewEncoder(w).Encode(response) +} + type CreateGroup500JSONResponse Error func (response CreateGroup500JSONResponse) VisitCreateGroupResponse(w http.ResponseWriter) error { @@ -2849,6 +2867,15 @@ func (response DeleteGroup401JSONResponse) VisitDeleteGroupResponse(w http.Respo return json.NewEncoder(w).Encode(response) } +type DeleteGroup403JSONResponse Error + +func (response DeleteGroup403JSONResponse) VisitDeleteGroupResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(403) + + return json.NewEncoder(w).Encode(response) +} + type DeleteGroup404JSONResponse Error func (response DeleteGroup404JSONResponse) VisitDeleteGroupResponse(w http.ResponseWriter) error { @@ -2893,6 +2920,15 @@ func (response GetGroup401JSONResponse) VisitGetGroupResponse(w http.ResponseWri return json.NewEncoder(w).Encode(response) } +type GetGroup403JSONResponse Error + +func (response GetGroup403JSONResponse) VisitGetGroupResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(403) + + return json.NewEncoder(w).Encode(response) +} + type GetGroup404JSONResponse Error func (response GetGroup404JSONResponse) VisitGetGroupResponse(w http.ResponseWriter) error { @@ -2947,6 +2983,15 @@ func (response UpdateGroup401JSONResponse) VisitUpdateGroupResponse(w http.Respo return json.NewEncoder(w).Encode(response) } +type UpdateGroup403JSONResponse Error + +func (response UpdateGroup403JSONResponse) VisitUpdateGroupResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(403) + + return json.NewEncoder(w).Encode(response) +} + type UpdateGroup404JSONResponse Error func (response UpdateGroup404JSONResponse) VisitUpdateGroupResponse(w http.ResponseWriter) error { @@ -2991,6 +3036,15 @@ func (response ListGroupMembers401JSONResponse) VisitListGroupMembersResponse(w return json.NewEncoder(w).Encode(response) } +type ListGroupMembers403JSONResponse Error + +func (response ListGroupMembers403JSONResponse) VisitListGroupMembersResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(403) + + return json.NewEncoder(w).Encode(response) +} + type ListGroupMembers404JSONResponse Error func (response ListGroupMembers404JSONResponse) VisitListGroupMembersResponse(w http.ResponseWriter) error { @@ -3045,6 +3099,15 @@ func (response CreateGroupMember401JSONResponse) VisitCreateGroupMemberResponse( return json.NewEncoder(w).Encode(response) } +type CreateGroupMember403JSONResponse Error + +func (response CreateGroupMember403JSONResponse) VisitCreateGroupMemberResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(403) + + return json.NewEncoder(w).Encode(response) +} + type CreateGroupMember404JSONResponse Error func (response CreateGroupMember404JSONResponse) VisitCreateGroupMemberResponse(w http.ResponseWriter) error { @@ -3107,6 +3170,15 @@ func (response RemoveGroupMember401JSONResponse) VisitRemoveGroupMemberResponse( return json.NewEncoder(w).Encode(response) } +type RemoveGroupMember403JSONResponse Error + +func (response RemoveGroupMember403JSONResponse) VisitRemoveGroupMemberResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(403) + + return json.NewEncoder(w).Encode(response) +} + type RemoveGroupMember404JSONResponse Error func (response RemoveGroupMember404JSONResponse) VisitRemoveGroupMemberResponse(w http.ResponseWriter) error { @@ -3162,6 +3234,15 @@ func (response UpdateGroupMember401JSONResponse) VisitUpdateGroupMemberResponse( return json.NewEncoder(w).Encode(response) } +type UpdateGroupMember403JSONResponse Error + +func (response UpdateGroupMember403JSONResponse) VisitUpdateGroupMemberResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(403) + + return json.NewEncoder(w).Encode(response) +} + type UpdateGroupMember404JSONResponse Error func (response UpdateGroupMember404JSONResponse) VisitUpdateGroupMemberResponse(w http.ResponseWriter) error { diff --git a/internal/api_server/server.go b/internal/api_server/server.go index 9a0aa2993..67aa98c71 100644 --- a/internal/api_server/server.go +++ b/internal/api_server/server.go @@ -172,18 +172,32 @@ func (s *Server) Run(ctx context.Context) error { } sizerClient := client.NewSizerClient(s.cfg.Service.Sizer.ServiceURL, sizerTimeout) - accountsSvc := service.NewAccountsService(s.store) + innerAccountsSvc := service.NewAccountsService(s.store) - var assessmentSvc service.AssessmentServicer - assessmentSvc = service.NewAssessmentService(s.store, s.opaValidator, accountsSvc) - if s.cfg.Service.Auth.AuthenticationType != "none" { - assessmentSvc = service.NewAuthzAssessmentService(assessmentSvc, s.store, accountsSvc) + if s.cfg.Service.AdminGroupFile != "" { + adminGroup, err := service.ParseAdminGroupFile(s.cfg.Service.AdminGroupFile) + if err != nil { + return fmt.Errorf("failed to load admin group: %w", err) + } + if err := innerAccountsSvc.Initialize(ctx, *adminGroup); err != nil { + return fmt.Errorf("failed to initialize admin group: %w", err) + } + zap.S().Named("api_server").Infof("Admin group %q initialized with %d members", adminGroup.Name, len(adminGroup.Members)) } - var partnerSvc service.PartnerServicer - partnerSvc = service.NewPartnerService(s.store, accountsSvc) + var ( + partnerSvc service.PartnerServicer + assessmentSvc service.AssessmentServicer + accountsSvc service.AccountsServicer + ) + partnerSvc = service.NewPartnerService(s.store, innerAccountsSvc) + assessmentSvc = service.NewAssessmentService(s.store, s.opaValidator, innerAccountsSvc) + accountsSvc = innerAccountsSvc + if s.cfg.Service.Auth.AuthenticationType != "none" { - partnerSvc = service.NewAuthzPartnerService(partnerSvc, accountsSvc, s.store) + partnerSvc = service.NewAuthzPartnerService(partnerSvc, innerAccountsSvc, s.store) + assessmentSvc = service.NewAuthzAssessmentService(assessmentSvc, s.store, innerAccountsSvc) + accountsSvc = service.NewAuthzAccountsService(accountsSvc) } h := handlers.NewServiceHandler( @@ -192,9 +206,10 @@ func (s *Server) Run(ctx context.Context) error { service.NewJobService(s.store, s.jobsClient.RiverClient, s.jobsClient.Pool), service.NewSizerService(sizerClient, s.store), service.NewEstimationService(s.store), - accountsSvc, partnerSvc, + accountsSvc, ) + server.HandlerFromMux(server.NewStrictHandler(h, nil), router) srv := http.Server{Addr: s.cfg.Service.Address, Handler: router} diff --git a/internal/config/config.go b/internal/config/config.go index d5e01fd85..ff04ad71e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -33,6 +33,7 @@ type svcConfig struct { OpaPoliciesFolder string `envconfig:"MIGRATION_PLANNER_OPA_POLICIES_FOLDER" default:"/app/policies"` IsoPath string `envconfig:"MIGRATION_PLANNER_ISO_PATH" default:"rhcos-live-iso.x86_64.iso"` Sizer Sizer + AdminGroupFile string `envconfig:"MIGRATION_PLANNER_ADMIN_GROUP_FILE" default:""` } type Auth struct { diff --git a/internal/handlers/v1alpha1/accounts.go b/internal/handlers/v1alpha1/accounts.go index 42a3fdf5e..57a51a2ff 100644 --- a/internal/handlers/v1alpha1/accounts.go +++ b/internal/handlers/v1alpha1/accounts.go @@ -53,8 +53,14 @@ func (h *ServiceHandler) ListGroups(ctx context.Context, request server.ListGrou groups, err := h.accountsSrv.ListGroups(ctx, filter) if err != nil { - logger.Error(err).Log() - return server.ListGroups500JSONResponse{Message: fmt.Sprintf("failed to list groups: %v", err)}, nil + switch err.(type) { + case *service.ErrForbidden: + logger.Error(err).Log() + return server.ListGroups403JSONResponse{Message: "you do not have permission to perform this action"}, nil + default: + logger.Error(err).Log() + return server.ListGroups500JSONResponse{Message: fmt.Sprintf("failed to list groups: %v", err)}, nil + } } return server.ListGroups200JSONResponse(mappers.GroupListToApi(groups)), nil @@ -83,6 +89,9 @@ func (h *ServiceHandler) CreateGroup(ctx context.Context, request server.CreateG created, err := h.accountsSrv.CreateGroup(ctx, group) if err != nil { switch err.(type) { + case *service.ErrForbidden: + logger.Error(err).Log() + return server.CreateGroup403JSONResponse{Message: "you do not have permission to perform this action"}, nil case *service.ErrDuplicateKey: return server.CreateGroup400JSONResponse{Message: "group already exists"}, nil default: @@ -106,6 +115,9 @@ func (h *ServiceHandler) GetGroup(ctx context.Context, request server.GetGroupRe group, err := h.accountsSrv.GetGroup(ctx, request.Id) if err != nil { switch err.(type) { + case *service.ErrForbidden: + logger.Error(err).Log() + return server.GetGroup403JSONResponse{Message: "you do not have permission to perform this action"}, nil case *service.ErrResourceNotFound: return server.GetGroup404JSONResponse{Message: "group not found"}, nil default: @@ -140,6 +152,9 @@ func (h *ServiceHandler) UpdateGroup(ctx context.Context, request server.UpdateG existing, err := h.accountsSrv.GetGroup(ctx, request.Id) if err != nil { switch err.(type) { + case *service.ErrForbidden: + logger.Error(err).Log() + return server.UpdateGroup403JSONResponse{Message: "you do not have permission to perform this action"}, nil case *service.ErrResourceNotFound: return server.UpdateGroup404JSONResponse{Message: "group not found"}, nil default: @@ -152,6 +167,9 @@ func (h *ServiceHandler) UpdateGroup(ctx context.Context, request server.UpdateG result, err := h.accountsSrv.UpdateGroup(ctx, updated) if err != nil { switch err.(type) { + case *service.ErrForbidden: + logger.Error(err).Log() + return server.UpdateGroup403JSONResponse{Message: "you do not have permission to perform this action"}, nil case *service.ErrDuplicateKey: return server.UpdateGroup400JSONResponse{Message: "group already exists"}, nil default: @@ -175,6 +193,9 @@ func (h *ServiceHandler) DeleteGroup(ctx context.Context, request server.DeleteG group, err := h.accountsSrv.GetGroup(ctx, request.Id) if err != nil { switch err.(type) { + case *service.ErrForbidden: + logger.Error(err).Log() + return server.DeleteGroup403JSONResponse{Message: "you do not have permission to perform this action"}, nil case *service.ErrResourceNotFound: return server.DeleteGroup404JSONResponse{Message: "group not found"}, nil default: @@ -184,8 +205,14 @@ func (h *ServiceHandler) DeleteGroup(ctx context.Context, request server.DeleteG } if err := h.accountsSrv.DeleteGroup(ctx, request.Id); err != nil { - logger.Error(err).Log() - return server.DeleteGroup500JSONResponse{Message: fmt.Sprintf("failed to delete group: %v", err)}, nil + switch err.(type) { + case *service.ErrForbidden: + logger.Error(err).Log() + return server.DeleteGroup403JSONResponse{Message: "you do not have permission to perform this action"}, nil + default: + logger.Error(err).Log() + return server.DeleteGroup500JSONResponse{Message: fmt.Sprintf("failed to delete group: %v", err)}, nil + } } logger.Success().WithString("group_name", group.Name).Log() @@ -203,6 +230,9 @@ func (h *ServiceHandler) ListGroupMembers(ctx context.Context, request server.Li members, err := h.accountsSrv.ListGroupMembers(ctx, request.Id) if err != nil { switch err.(type) { + case *service.ErrForbidden: + logger.Error(err).Log() + return server.ListGroupMembers403JSONResponse{Message: "you do not have permission to perform this action"}, nil case *service.ErrResourceNotFound: return server.ListGroupMembers404JSONResponse{Message: "group not found"}, nil default: @@ -231,6 +261,9 @@ func (h *ServiceHandler) UpdateGroupMember(ctx context.Context, request server.U existing, err := h.accountsSrv.GetMember(ctx, request.Username) if err != nil { switch err.(type) { + case *service.ErrForbidden: + logger.Error(err).Log() + return server.UpdateGroupMember403JSONResponse{Message: "you do not have permission to perform this action"}, nil case *service.ErrResourceNotFound: return server.UpdateGroupMember404JSONResponse{Message: err.Error()}, nil default: @@ -244,6 +277,9 @@ func (h *ServiceHandler) UpdateGroupMember(ctx context.Context, request server.U result, err := h.accountsSrv.UpdateGroupMember(ctx, request.Id, request.Username, updated) if err != nil { switch err.(type) { + case *service.ErrForbidden: + logger.Error(err).Log() + return server.UpdateGroupMember403JSONResponse{Message: "you do not have permission to perform this action"}, nil case *service.ErrResourceNotFound: return server.UpdateGroupMember404JSONResponse{Message: err.Error()}, nil case *service.ErrMembershipMismatch: @@ -270,6 +306,9 @@ func (h *ServiceHandler) RemoveGroupMember(ctx context.Context, request server.R err := h.accountsSrv.RemoveGroupMember(ctx, request.Id, request.Username) if err != nil { switch err.(type) { + case *service.ErrForbidden: + logger.Error(err).Log() + return server.RemoveGroupMember403JSONResponse{Message: "you do not have permission to perform this action"}, nil case *service.ErrResourceNotFound: return server.RemoveGroupMember404JSONResponse{Message: err.Error()}, nil case *service.ErrMembershipMismatch: @@ -301,6 +340,9 @@ func (h *ServiceHandler) CreateGroupMember(ctx context.Context, request server.C created, err := h.accountsSrv.CreateMember(ctx, member) if err != nil { switch err.(type) { + case *service.ErrForbidden: + logger.Error(err).Log() + return server.CreateGroupMember403JSONResponse{Message: "you do not have permission to perform this action"}, nil case *service.ErrResourceNotFound: return server.CreateGroupMember404JSONResponse{Message: "group not found"}, nil case *service.ErrDuplicateKey: diff --git a/internal/handlers/v1alpha1/accounts_test.go b/internal/handlers/v1alpha1/accounts_test.go index 692b8c431..f9c90833e 100644 --- a/internal/handlers/v1alpha1/accounts_test.go +++ b/internal/handlers/v1alpha1/accounts_test.go @@ -40,7 +40,7 @@ var _ = Describe("accounts handler", Ordered, func() { s = store.NewStore(db) gormdb = db accountsSvc := service.NewAccountsService(s) - srv = handlers.NewServiceHandler(nil, nil, nil, nil, nil, accountsSvc, service.NewPartnerService(s, accountsSvc)) + srv = handlers.NewServiceHandler(nil, nil, nil, nil, nil, service.NewPartnerService(s, accountsSvc), accountsSvc) }) AfterAll(func() { @@ -671,4 +671,78 @@ var _ = Describe("accounts handler", Ordered, func() { }) }) }) + + Context("Authz 403 responses", func() { + var authzSrv *handlers.ServiceHandler + + BeforeAll(func() { + accountsSvc := service.NewAccountsService(s) + authzAccountsSvc := service.NewAuthzAccountsService(accountsSvc) + authzSrv = handlers.NewServiceHandler(nil, nil, nil, nil, nil, service.NewPartnerService(s, accountsSvc), authzAccountsSvc) + }) + + nonAdminCtx := func() context.Context { + return auth.NewTokenContext(context.TODO(), auth.User{Username: "regularuser", Organization: "org"}) + } + + It("ListGroups returns 403 for non-admin", func() { + resp, err := authzSrv.ListGroups(nonAdminCtx(), server.ListGroupsRequestObject{}) + Expect(err).To(BeNil()) + Expect(resp).To(BeAssignableToTypeOf(server.ListGroups403JSONResponse{})) + }) + + It("CreateGroup returns 403 for non-admin", func() { + body := v1alpha1.GroupCreate{Name: "X", Kind: v1alpha1.GroupCreateKindPartner, Icon: "i", Company: "C"} + resp, err := authzSrv.CreateGroup(nonAdminCtx(), server.CreateGroupRequestObject{Body: &body}) + Expect(err).To(BeNil()) + Expect(resp).To(BeAssignableToTypeOf(server.CreateGroup403JSONResponse{})) + }) + + It("GetGroup returns 403 for non-admin", func() { + resp, err := authzSrv.GetGroup(nonAdminCtx(), server.GetGroupRequestObject{Id: uuid.New()}) + Expect(err).To(BeNil()) + Expect(resp).To(BeAssignableToTypeOf(server.GetGroup403JSONResponse{})) + }) + + It("UpdateGroup returns 403 for non-admin", func() { + name := "X" + body := v1alpha1.GroupUpdate{Name: &name} + resp, err := authzSrv.UpdateGroup(nonAdminCtx(), server.UpdateGroupRequestObject{Id: uuid.New(), Body: &body}) + Expect(err).To(BeNil()) + Expect(resp).To(BeAssignableToTypeOf(server.UpdateGroup403JSONResponse{})) + }) + + It("DeleteGroup returns 403 for non-admin", func() { + resp, err := authzSrv.DeleteGroup(nonAdminCtx(), server.DeleteGroupRequestObject{Id: uuid.New()}) + Expect(err).To(BeNil()) + Expect(resp).To(BeAssignableToTypeOf(server.DeleteGroup403JSONResponse{})) + }) + + It("ListGroupMembers returns 403 for non-admin", func() { + resp, err := authzSrv.ListGroupMembers(nonAdminCtx(), server.ListGroupMembersRequestObject{Id: uuid.New()}) + Expect(err).To(BeNil()) + Expect(resp).To(BeAssignableToTypeOf(server.ListGroupMembers403JSONResponse{})) + }) + + It("CreateGroupMember returns 403 for non-admin", func() { + body := v1alpha1.MemberCreate{Username: "u", Email: "u@x.com"} + resp, err := authzSrv.CreateGroupMember(nonAdminCtx(), server.CreateGroupMemberRequestObject{Id: uuid.New(), Body: &body}) + Expect(err).To(BeNil()) + Expect(resp).To(BeAssignableToTypeOf(server.CreateGroupMember403JSONResponse{})) + }) + + It("UpdateGroupMember returns 403 for non-admin", func() { + email := openapi_types.Email("u@x.com") + body := v1alpha1.MemberUpdate{Email: &email} + resp, err := authzSrv.UpdateGroupMember(nonAdminCtx(), server.UpdateGroupMemberRequestObject{Id: uuid.New(), Username: "u", Body: &body}) + Expect(err).To(BeNil()) + Expect(resp).To(BeAssignableToTypeOf(server.UpdateGroupMember403JSONResponse{})) + }) + + It("RemoveGroupMember returns 403 for non-admin", func() { + resp, err := authzSrv.RemoveGroupMember(nonAdminCtx(), server.RemoveGroupMemberRequestObject{Id: uuid.New(), Username: "u"}) + Expect(err).To(BeNil()) + Expect(resp).To(BeAssignableToTypeOf(server.RemoveGroupMember403JSONResponse{})) + }) + }) }) diff --git a/internal/handlers/v1alpha1/partner_test.go b/internal/handlers/v1alpha1/partner_test.go index 06a6c3c4d..e8932b89f 100644 --- a/internal/handlers/v1alpha1/partner_test.go +++ b/internal/handlers/v1alpha1/partner_test.go @@ -42,7 +42,7 @@ var _ = Describe("partner handler", Ordered, func() { gormdb = db accountsSvc := service.NewAccountsService(s) partnerSvc := service.NewAuthzPartnerService(service.NewPartnerService(s, accountsSvc), accountsSvc, s) - srv = handlers.NewServiceHandler(nil, nil, nil, nil, nil, accountsSvc, partnerSvc) + srv = handlers.NewServiceHandler(nil, nil, nil, nil, nil, partnerSvc, accountsSvc) }) AfterAll(func() { diff --git a/internal/handlers/v1alpha1/source.go b/internal/handlers/v1alpha1/source.go index ece9ad536..ab66f8915 100644 --- a/internal/handlers/v1alpha1/source.go +++ b/internal/handlers/v1alpha1/source.go @@ -21,8 +21,8 @@ type ServiceHandler struct { jobSrv *service.JobService sizerSrv *service.SizerService estimationSrv *service.EstimationService - accountsSrv *service.AccountsService partnerSrv service.PartnerServicer + accountsSrv service.AccountsServicer } func NewServiceHandler( @@ -31,8 +31,8 @@ func NewServiceHandler( j *service.JobService, sizer *service.SizerService, estimation *service.EstimationService, - accounts *service.AccountsService, partner service.PartnerServicer, + accounts service.AccountsServicer, ) *ServiceHandler { return &ServiceHandler{ sourceSrv: sourceService, diff --git a/internal/service/accounts.go b/internal/service/accounts.go index 67cee0dd0..a1ae30414 100644 --- a/internal/service/accounts.go +++ b/internal/service/accounts.go @@ -4,13 +4,88 @@ import ( "context" "errors" "fmt" + "net/mail" + "os" + "strings" "github.com/google/uuid" "github.com/kubev2v/migration-planner/internal/auth" "github.com/kubev2v/migration-planner/internal/store" "github.com/kubev2v/migration-planner/internal/store/model" + "gopkg.in/yaml.v3" ) +type AccountsServicer interface { + Initialize(ctx context.Context, adminGroup AdminGroup) error + GetIdentity(ctx context.Context, authUser auth.User) (Identity, error) + ListGroups(ctx context.Context, filter *store.GroupQueryFilter) (model.GroupList, error) + GetGroup(ctx context.Context, id uuid.UUID) (model.Group, error) + CreateGroup(ctx context.Context, group model.Group) (model.Group, error) + UpdateGroup(ctx context.Context, group model.Group) (model.Group, error) + DeleteGroup(ctx context.Context, id uuid.UUID) error + GetMember(ctx context.Context, username string) (model.Member, error) + ListGroupMembers(ctx context.Context, groupID uuid.UUID) (model.MemberList, error) + CreateMember(ctx context.Context, member model.Member) (model.Member, error) + UpdateGroupMember(ctx context.Context, groupID uuid.UUID, username string, member model.Member) (model.Member, error) + RemoveGroupMember(ctx context.Context, groupID uuid.UUID, username string) error +} + +type AdminGroupMember struct { + Username string `yaml:"username"` + Email string `yaml:"email"` +} + +type AdminGroup struct { + Name string `yaml:"name"` + Members []AdminGroupMember `yaml:"members"` +} + +func ParseAdminGroupFile(path string) (*AdminGroup, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("reading admin group file: %w", err) + } + + var ag AdminGroup + if err := yaml.Unmarshal(data, &ag); err != nil { + return nil, fmt.Errorf("parsing admin group file: %w", err) + } + + ag.Name = strings.TrimSpace(ag.Name) + if ag.Name == "" { + return nil, fmt.Errorf("admin group file: name is required") + } + if len(ag.Members) == 0 { + return nil, fmt.Errorf("admin group file: at least one member is required") + } + + members := make(map[string]AdminGroupMember, len(ag.Members)) + for i, m := range ag.Members { + m.Username = strings.TrimSpace(m.Username) + m.Email = strings.TrimSpace(m.Email) + if m.Username == "" { + return nil, fmt.Errorf("admin group file: member[%d] username is required", i) + } + if m.Email == "" { + return nil, fmt.Errorf("admin group file: member[%d] email is required", i) + } + if _, err := mail.ParseAddress(m.Email); err != nil { + return nil, fmt.Errorf("admin group file: member[%d] invalid email %q: %w", i, m.Email, err) + } + if _, exists := members[m.Username]; exists { + continue + } + members[m.Username] = m + } + + ag.Members = make([]AdminGroupMember, 0, len(members)) + for _, m := range members { + ag.Members = append(ag.Members, m) + } + + return &ag, nil +} + type AccountsService struct { store store.Store } @@ -19,6 +94,10 @@ func NewAccountsService(store store.Store) *AccountsService { return &AccountsService{store: store} } +func NewAccountsServicer(store store.Store) AccountsServicer { + return &AccountsService{store: store} +} + // Identity type IdentityKind string @@ -77,6 +156,55 @@ func (s *AccountsService) GetIdentity(ctx context.Context, authUser auth.User) ( return Identity{Username: authUser.Username, Kind: KindRegular}, nil } +func (s *AccountsService) Initialize(ctx context.Context, adminGroup AdminGroup) (retErr error) { + ctx, err := s.store.NewTransactionContext(ctx) + if err != nil { + return fmt.Errorf("starting transaction: %w", err) + } + defer func() { + if retErr != nil { + _, _ = store.Rollback(ctx) + } + }() + + groups, err := s.store.Accounts().ListGroups(ctx, + store.NewGroupQueryFilter().ByName(adminGroup.Name).ByKind("admin")) + if err != nil { + return fmt.Errorf("looking up admin group: %w", err) + } + + for _, g := range groups { + if err := s.store.Accounts().DeleteGroup(ctx, g.ID); err != nil { + return fmt.Errorf("deleting existing admin group %s: %w", g.ID, err) + } + } + + group, err := s.store.Accounts().CreateGroup(ctx, model.Group{ + ID: uuid.New(), + Name: adminGroup.Name, + Company: adminGroup.Name, + Kind: "admin", + }) + if err != nil { + return fmt.Errorf("creating admin group: %w", err) + } + + for _, m := range adminGroup.Members { + if _, err := s.store.Accounts().CreateMember(ctx, model.Member{ + Username: m.Username, + Email: m.Email, + GroupID: group.ID, + }); err != nil { + return fmt.Errorf("adding admin member %s: %w", m.Username, err) + } + } + + if _, err := store.Commit(ctx); err != nil { + return fmt.Errorf("committing transaction: %w", err) + } + return nil +} + func (s *AccountsService) ListGroups(ctx context.Context, filter *store.GroupQueryFilter) (model.GroupList, error) { return s.store.Accounts().ListGroups(ctx, filter) } diff --git a/internal/service/accounts_test.go b/internal/service/accounts_test.go index 7f83aa179..2fa995fea 100644 --- a/internal/service/accounts_test.go +++ b/internal/service/accounts_test.go @@ -3,6 +3,7 @@ package service_test import ( "context" "fmt" + "os" "github.com/google/uuid" "github.com/kubev2v/migration-planner/internal/auth" @@ -24,7 +25,7 @@ var _ = Describe("accounts service", Ordered, func() { var ( s store.Store gormdb *gorm.DB - svc *service.AccountsService + svc service.AccountsServicer ) BeforeAll(func() { @@ -35,7 +36,7 @@ var _ = Describe("accounts service", Ordered, func() { s = store.NewStore(db) gormdb = db - svc = service.NewAccountsService(s) + svc = service.NewAccountsServicer(s) }) AfterAll(func() { @@ -374,6 +375,158 @@ var _ = Describe("accounts service", Ordered, func() { }) }) + Context("Initialize", func() { + It("creates admin group and members from scratch", func() { + ag := service.AdminGroup{ + Name: "test-admins", + Members: []service.AdminGroupMember{ + {Username: "alice", Email: "alice@rh.com"}, + {Username: "bob", Email: "bob@rh.com"}, + }, + } + + err := svc.Initialize(context.TODO(), ag) + Expect(err).To(BeNil()) + + groups, err := svc.ListGroups(context.TODO(), nil) + Expect(err).To(BeNil()) + Expect(groups).To(HaveLen(1)) + Expect(groups[0].Name).To(Equal("test-admins")) + Expect(groups[0].Company).To(Equal("test-admins")) + Expect(groups[0].Kind).To(Equal("admin")) + Expect(groups[0].Members).To(HaveLen(2)) + }) + + It("replaces existing admin group on re-initialize", func() { + ag := service.AdminGroup{ + Name: "test-admins", + Members: []service.AdminGroupMember{ + {Username: "alice", Email: "alice@rh.com"}, + }, + } + err := svc.Initialize(context.TODO(), ag) + Expect(err).To(BeNil()) + + ag2 := service.AdminGroup{ + Name: "test-admins", + Members: []service.AdminGroupMember{ + {Username: "bob", Email: "bob@rh.com"}, + {Username: "carol", Email: "carol@rh.com"}, + }, + } + err = svc.Initialize(context.TODO(), ag2) + Expect(err).To(BeNil()) + + groups, err := svc.ListGroups(context.TODO(), nil) + Expect(err).To(BeNil()) + Expect(groups).To(HaveLen(1)) + Expect(groups[0].Members).To(HaveLen(2)) + + usernames := []string{groups[0].Members[0].Username, groups[0].Members[1].Username} + Expect(usernames).To(ContainElements("bob", "carol")) + }) + + It("removes stale members on re-initialize", func() { + ag := service.AdminGroup{ + Name: "test-admins", + Members: []service.AdminGroupMember{ + {Username: "alice", Email: "alice@rh.com"}, + {Username: "bob", Email: "bob@rh.com"}, + {Username: "carol", Email: "carol@rh.com"}, + }, + } + err := svc.Initialize(context.TODO(), ag) + Expect(err).To(BeNil()) + + ag2 := service.AdminGroup{ + Name: "test-admins", + Members: []service.AdminGroupMember{ + {Username: "alice", Email: "alice@rh.com"}, + }, + } + err = svc.Initialize(context.TODO(), ag2) + Expect(err).To(BeNil()) + + groups, err := svc.ListGroups(context.TODO(), nil) + Expect(err).To(BeNil()) + Expect(groups).To(HaveLen(1)) + Expect(groups[0].Members).To(HaveLen(1)) + Expect(groups[0].Members[0].Username).To(Equal("alice")) + }) + + It("preserves member email from config", func() { + ag := service.AdminGroup{ + Name: "test-admins", + Members: []service.AdminGroupMember{ + {Username: "alice", Email: "alice@redhat.com"}, + }, + } + + err := svc.Initialize(context.TODO(), ag) + Expect(err).To(BeNil()) + + member, err := svc.GetMember(context.TODO(), "alice") + Expect(err).To(BeNil()) + Expect(member.Email).To(Equal("alice@redhat.com")) + }) + + AfterEach(func() { + gormdb.Exec("DELETE FROM members;") + gormdb.Exec("DELETE FROM groups;") + }) + }) + + Context("ParseAdminGroupFile", func() { + writeTempYAML := func(content string) string { + f, err := os.CreateTemp("", "admin-group-*.yaml") + Expect(err).To(BeNil()) + _, err = f.WriteString(content) + Expect(err).To(BeNil()) + Expect(f.Close()).To(BeNil()) + DeferCleanup(func() { _ = os.Remove(f.Name()) }) + return f.Name() + } + + It("parses a valid file", func() { + path := writeTempYAML("name: test-admins\nmembers:\n - username: alice\n email: alice@rh.com\n") + ag, err := service.ParseAdminGroupFile(path) + Expect(err).To(BeNil()) + Expect(ag.Name).To(Equal("test-admins")) + Expect(ag.Members).To(HaveLen(1)) + Expect(ag.Members[0].Username).To(Equal("alice")) + Expect(ag.Members[0].Email).To(Equal("alice@rh.com")) + }) + + It("returns error for missing file", func() { + _, err := service.ParseAdminGroupFile("/nonexistent/path.yaml") + Expect(err).ToNot(BeNil()) + }) + + It("deduplicates members by username", func() { + path := writeTempYAML("name: test-admins\nmembers:\n - username: alice\n email: alice@rh.com\n - username: alice\n email: alice2@rh.com\n") + ag, err := service.ParseAdminGroupFile(path) + Expect(err).To(BeNil()) + Expect(ag.Members).To(HaveLen(1)) + }) + + DescribeTable("returns error for invalid input", + func(content string, expectedErr string) { + path := writeTempYAML(content) + _, err := service.ParseAdminGroupFile(path) + Expect(err).ToNot(BeNil()) + Expect(err.Error()).To(ContainSubstring(expectedErr)) + }, + Entry("empty name", "name: \"\"\nmembers:\n - username: alice\n email: alice@rh.com\n", "name is required"), + Entry("whitespace name", "name: \" \"\nmembers:\n - username: alice\n email: alice@rh.com\n", "name is required"), + Entry("empty members", "name: test-admins\nmembers: []\n", "at least one member"), + Entry("empty username", "name: test-admins\nmembers:\n - username: \"\"\n email: alice@rh.com\n", "username is required"), + Entry("whitespace username", "name: test-admins\nmembers:\n - username: \" \"\n email: alice@rh.com\n", "username is required"), + Entry("empty email", "name: test-admins\nmembers:\n - username: alice\n email: \"\"\n", "email is required"), + Entry("whitespace email", "name: test-admins\nmembers:\n - username: alice\n email: \" \"\n", "email is required"), + Entry("invalid email", "name: test-admins\nmembers:\n - username: alice\n email: not-an-email\n", "invalid email"), + ) + }) + Context("Membership", func() { Context("ListGroupMembers", func() { It("lists members in the group", func() { diff --git a/internal/service/authz_accounts.go b/internal/service/authz_accounts.go new file mode 100644 index 000000000..188e0b8af --- /dev/null +++ b/internal/service/authz_accounts.go @@ -0,0 +1,112 @@ +package service + +import ( + "context" + "fmt" + + "github.com/google/uuid" + "github.com/kubev2v/migration-planner/internal/auth" + "github.com/kubev2v/migration-planner/internal/store" + "github.com/kubev2v/migration-planner/internal/store/model" +) + +type AuthzAccountsService struct { + inner AccountsServicer +} + +func NewAuthzAccountsService(inner AccountsServicer) AccountsServicer { + return &AuthzAccountsService{inner: inner} +} + +// Initialize is bootstrap-only — called from api_server.Run before HTTP serving starts. +// It intentionally skips authz because no authenticated user exists at boot time. +// Must not be called from request handlers; use innerAccountsSvc.Initialize directly. +func (a *AuthzAccountsService) Initialize(ctx context.Context, adminGroup AdminGroup) error { + return a.inner.Initialize(ctx, adminGroup) +} + +func (a *AuthzAccountsService) GetIdentity(ctx context.Context, authUser auth.User) (Identity, error) { + return a.inner.GetIdentity(ctx, authUser) +} + +func (a *AuthzAccountsService) ListGroups(ctx context.Context, filter *store.GroupQueryFilter) (model.GroupList, error) { + if err := a.requireAdmin(ctx, "groups"); err != nil { + return nil, err + } + return a.inner.ListGroups(ctx, filter) +} + +func (a *AuthzAccountsService) GetGroup(ctx context.Context, id uuid.UUID) (model.Group, error) { + if err := a.requireAdmin(ctx, "groups"); err != nil { + return model.Group{}, err + } + return a.inner.GetGroup(ctx, id) +} + +func (a *AuthzAccountsService) CreateGroup(ctx context.Context, group model.Group) (model.Group, error) { + if err := a.requireAdmin(ctx, "groups"); err != nil { + return model.Group{}, err + } + return a.inner.CreateGroup(ctx, group) +} + +func (a *AuthzAccountsService) UpdateGroup(ctx context.Context, group model.Group) (model.Group, error) { + if err := a.requireAdmin(ctx, "groups"); err != nil { + return model.Group{}, err + } + return a.inner.UpdateGroup(ctx, group) +} + +func (a *AuthzAccountsService) DeleteGroup(ctx context.Context, id uuid.UUID) error { + if err := a.requireAdmin(ctx, "groups"); err != nil { + return err + } + return a.inner.DeleteGroup(ctx, id) +} + +func (a *AuthzAccountsService) GetMember(ctx context.Context, username string) (model.Member, error) { + if err := a.requireAdmin(ctx, "members"); err != nil { + return model.Member{}, err + } + return a.inner.GetMember(ctx, username) +} + +func (a *AuthzAccountsService) ListGroupMembers(ctx context.Context, groupID uuid.UUID) (model.MemberList, error) { + if err := a.requireAdmin(ctx, "members"); err != nil { + return nil, err + } + return a.inner.ListGroupMembers(ctx, groupID) +} + +func (a *AuthzAccountsService) CreateMember(ctx context.Context, member model.Member) (model.Member, error) { + if err := a.requireAdmin(ctx, "members"); err != nil { + return model.Member{}, err + } + return a.inner.CreateMember(ctx, member) +} + +func (a *AuthzAccountsService) UpdateGroupMember(ctx context.Context, groupID uuid.UUID, username string, member model.Member) (model.Member, error) { + if err := a.requireAdmin(ctx, "members"); err != nil { + return model.Member{}, err + } + return a.inner.UpdateGroupMember(ctx, groupID, username, member) +} + +func (a *AuthzAccountsService) RemoveGroupMember(ctx context.Context, groupID uuid.UUID, username string) error { + if err := a.requireAdmin(ctx, "members"); err != nil { + return err + } + return a.inner.RemoveGroupMember(ctx, groupID, username) +} + +func (a *AuthzAccountsService) requireAdmin(ctx context.Context, resource string) error { + user := auth.MustHaveUser(ctx) + identity, err := a.inner.GetIdentity(ctx, user) + if err != nil { + return fmt.Errorf("authz: failed to get identity: %w", err) + } + if identity.Kind != KindAdmin { + return NewErrForbidden(resource, user.Username) + } + return nil +} diff --git a/internal/service/partner_test.go b/internal/service/partner_test.go index 828a106fe..ef870d248 100644 --- a/internal/service/partner_test.go +++ b/internal/service/partner_test.go @@ -267,7 +267,7 @@ var _ = Describe("partner service", Ordered, func() { Expect(ok).To(BeTrue()) // User is still regular - accountsSvc := service.NewAccountsService(s) + accountsSvc := service.NewAccountsServicer(s) identity, err := accountsSvc.GetIdentity(context.TODO(), user) Expect(err).To(BeNil()) Expect(identity.Kind).To(Equal(service.KindRegular))