diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 095e2077d..bb158bd26 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -3478,6 +3478,39 @@ paths: description: The robot account is not found. '500': description: Unexpected internal errors. + '/system/oidc/ping': + post: + summary: Test the OIDC endpoint. + description: Test the OIDC endpoint, the setting of the endpoint is provided in the request. This API can only + be called by system admin. + tags: + - Products + - System + parameters: + - name: endpoint + in: body + description: Request body for OIDC endpoint to be tested. + required: true + schema: + type: object + properties: + url: + type: string + description: The URL of OIDC endpoint to be tested. + verify_cert: + type: boolean + description: Whether the certificate should be verified + responses: + '200': + description: The specified robot account is successfully deleted. + '400': + description: The ping failed + '401': + description: User need to log in first. + '403': + description: User does not have permission to call this API + '500': + description: Unexpected internal errors. '/system/CVEWhitelist': get: summary: Get the system level whitelist of CVE. diff --git a/src/common/utils/oidc/helper.go b/src/common/utils/oidc/helper.go index a970d5942..30f14e209 100644 --- a/src/common/utils/oidc/helper.go +++ b/src/common/utils/oidc/helper.go @@ -206,3 +206,19 @@ func RefreshToken(ctx context.Context, token *Token) (*Token, error) { } return &Token{Token: *t, IDToken: it}, nil } + +// Conn wraps connection info of an OIDC endpoint +type Conn struct { + URL string `json:"url"` + VerifyCert bool `json:"verify_cert"` +} + +// TestEndpoint tests whether the endpoint is a valid OIDC endpoint. +// The nil return value indicates the success of the test +func TestEndpoint(conn Conn) error { + + // gooidc will try to call the discovery api when creating the provider and that's all we need to check + ctx := clientCtx(context.Background(), conn.VerifyCert) + _, err := gooidc.NewProvider(ctx, conn.URL) + return err +} diff --git a/src/common/utils/oidc/helper_test.go b/src/common/utils/oidc/helper_test.go index 8586e6301..d706836b8 100644 --- a/src/common/utils/oidc/helper_test.go +++ b/src/common/utils/oidc/helper_test.go @@ -97,3 +97,16 @@ func TestAuthCodeURL(t *testing.T) { assert.Equal(t, "offline", q.Get("access_type")) assert.False(t, strings.Contains(q.Get("scope"), "offline_access")) } + +func TestTestEndpoint(t *testing.T) { + c1 := Conn{ + URL: googleEndpoint, + VerifyCert: true, + } + c2 := Conn{ + URL: "https://www.baidu.com", + VerifyCert: false, + } + assert.Nil(t, TestEndpoint(c1)) + assert.NotNil(t, TestEndpoint(c2)) +} diff --git a/src/core/api/harborapi_test.go b/src/core/api/harborapi_test.go index eed976dfd..ed51699c8 100644 --- a/src/core/api/harborapi_test.go +++ b/src/core/api/harborapi_test.go @@ -145,6 +145,7 @@ func init() { beego.Router("/api/system/gc/schedule", &GCAPI{}, "get:Get;put:Put;post:Post") beego.Router("/api/system/scanAll/schedule", &ScanAllAPI{}, "get:Get;put:Put;post:Post") beego.Router("/api/system/CVEWhitelist", &SysCVEWhitelistAPI{}, "get:Get;put:Put") + beego.Router("/api/system/oidc/ping", &OIDCAPI{}, "post:Ping") beego.Router("/api/projects/:pid([0-9]+)/robots/", &RobotAPI{}, "post:Post;get:List") beego.Router("/api/projects/:pid([0-9]+)/robots/:id([0-9]+)", &RobotAPI{}, "get:Get;put:Put;delete:Delete") diff --git a/src/core/api/oidc.go b/src/core/api/oidc.go new file mode 100644 index 000000000..ed4688cf8 --- /dev/null +++ b/src/core/api/oidc.go @@ -0,0 +1,56 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "errors" + "github.com/goharbor/harbor/src/common/utils/log" + "github.com/goharbor/harbor/src/common/utils/oidc" +) + +// OIDCAPI handles the requests to /api/system/oidc/xxx +type OIDCAPI struct { + BaseController +} + +// Prepare validates the request initially +func (oa *OIDCAPI) Prepare() { + oa.BaseController.Prepare() + if !oa.SecurityCtx.IsAuthenticated() { + oa.SendUnAuthorizedError(errors.New("unauthorized")) + return + } + if !oa.SecurityCtx.IsSysAdmin() { + msg := "only system admin has permission to access this API" + log.Errorf(msg) + oa.SendForbiddenError(errors.New(msg)) + return + } +} + +// Ping will handles the request to test connection to OIDC endpoint +func (oa *OIDCAPI) Ping() { + var c oidc.Conn + if err := oa.DecodeJSONReq(&c); err != nil { + log.Error("Failed to decode JSON request.") + oa.SendBadRequestError(err) + return + } + if err := oidc.TestEndpoint(c); err != nil { + log.Errorf("Failed to verify connection: %+v, err: %v", c, err) + oa.SendBadRequestError(err) + return + } +} diff --git a/src/core/api/oidc_test.go b/src/core/api/oidc_test.go new file mode 100644 index 000000000..ec9ada990 --- /dev/null +++ b/src/core/api/oidc_test.go @@ -0,0 +1,69 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "github.com/goharbor/harbor/src/common/utils/oidc" + "net/http" + "testing" +) + +func TestOIDCAPI_Ping(t *testing.T) { + url := "/api/system/oidc/ping" + cases := []*codeCheckingCase{ + { // 401 + request: &testingRequest{ + method: http.MethodPost, + bodyJSON: oidc.Conn{}, + url: url, + }, + code: http.StatusUnauthorized, + }, + { // 403 + request: &testingRequest{ + method: http.MethodPost, + bodyJSON: oidc.Conn{}, + url: url, + credential: nonSysAdmin, + }, + code: http.StatusForbidden, + }, + { // 400 + request: &testingRequest{ + method: http.MethodPost, + bodyJSON: oidc.Conn{ + URL: "https://www.baidu.com", + VerifyCert: true, + }, + url: url, + credential: sysAdmin, + }, + code: http.StatusBadRequest, + }, + { // 200 + request: &testingRequest{ + method: http.MethodPost, + bodyJSON: oidc.Conn{ + URL: "https://accounts.google.com", + VerifyCert: true, + }, + url: url, + credential: sysAdmin, + }, + code: http.StatusOK, + }, + } + runCodeCheckingCases(t, cases...) +} diff --git a/src/core/router.go b/src/core/router.go index 344967c1d..5a857ee6f 100644 --- a/src/core/router.go +++ b/src/core/router.go @@ -97,6 +97,7 @@ func initRouters() { beego.Router("/api/system/gc/schedule", &api.GCAPI{}, "get:Get;put:Put;post:Post") beego.Router("/api/system/scanAll/schedule", &api.ScanAllAPI{}, "get:Get;put:Put;post:Post") beego.Router("/api/system/CVEWhitelist", &api.SysCVEWhitelistAPI{}, "get:Get;put:Put") + beego.Router("/api/system/oidc/ping", &api.OIDCAPI{}, "post:Ping") beego.Router("/api/logs", &api.LogAPI{})