Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You 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 codequality

import (
"time"

"github.com/apache/incubator-devlake/core/models/domainlayer"
)

type CqProjectMetricsHistory struct {
domainlayer.DomainEntity
ProjectKey string `gorm:"index;type:varchar(500)"`
AnalysisDate time.Time `gorm:"index"`
Coverage *float64
Ncloc *int
Bugs *int
ReliabilityRating string `gorm:"type:varchar(5)"`
CodeSmells *int
SqaleRating string `gorm:"type:varchar(5)"`
Complexity *int
CognitiveComplexity *int
Vulnerabilities *int
SecurityRating string `gorm:"type:varchar(5)"`
SecurityHotspots *int
DuplicatedLinesDensity *float64
}

func (CqProjectMetricsHistory) TableName() string {
return "cq_project_metrics_history"
}
1 change: 1 addition & 0 deletions backend/core/models/domainlayer/domaininfo/domaininfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ func GetDomainTablesInfo() []dal.Tabler {
&codequality.CqIssue{},
&codequality.CqIssueImpact{},
&codequality.CqProject{},
&codequality.CqProjectMetricsHistory{},
// crossdomain
&crossdomain.Account{},
&crossdomain.BoardRepo{},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You 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 migrationscripts

import (
"github.com/apache/incubator-devlake/core/context"
"github.com/apache/incubator-devlake/core/errors"
"github.com/apache/incubator-devlake/core/models/migrationscripts/archived"
"github.com/apache/incubator-devlake/helpers/migrationhelper"
)

type addCqProjectMetricsHistory struct{}

func (u *addCqProjectMetricsHistory) Up(basicRes context.BasicRes) errors.Error {
return migrationhelper.AutoMigrateTables(
basicRes,
&archived.CqProjectMetricsHistory{},
)
}

func (*addCqProjectMetricsHistory) Version() uint64 {
return 20260707153201
}

func (*addCqProjectMetricsHistory) Name() string {
return "add cq_project_metrics_history domain table"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You 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 archived

import "time"

type CqProjectMetricsHistory struct {
DomainEntity
ProjectKey string `gorm:"index;type:varchar(500)"`
AnalysisDate time.Time `gorm:"index"`
Coverage *float64
Ncloc *int
Bugs *int
ReliabilityRating string `gorm:"type:varchar(5)"`
CodeSmells *int
SqaleRating string `gorm:"type:varchar(5)"`
Complexity *int
CognitiveComplexity *int
Vulnerabilities *int
SecurityRating string `gorm:"type:varchar(5)"`
SecurityHotspots *int
DuplicatedLinesDensity *float64
}

func (CqProjectMetricsHistory) TableName() string {
return "cq_project_metrics_history"
}
1 change: 1 addition & 0 deletions backend/core/models/migrationscripts/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,5 +145,6 @@ func All() []plugin.MigrationScript {
new(modifyCicdDeploymentsToText),
new(increaseCqIssuesProjectKeyLength),
new(addAuthSessions),
new(addCqProjectMetricsHistory),
}
}
3 changes: 3 additions & 0 deletions backend/plugins/sonarqube/api/connection_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ func testConnection(ctx context.Context, connection models.SonarqubeConn) (*plug
return nil, errors.Default.Wrap(err, "error validating target")
}
}
if err := connection.ValidateUserTokenPrefix(); err != nil {
return nil, err
}
apiClient, err := api.NewApiClientFromConnection(ctx, basicRes, &connection)
if err != nil {
return nil, err
Expand Down
7 changes: 7 additions & 0 deletions backend/plugins/sonarqube/impl/impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ func (p Sonarqube) GetTablesInfo() []dal.Tabler {
&models.SonarqubeFileMetrics{},
&models.SonarqubeAccount{},
&models.SonarqubeScopeConfig{},
&models.SonarqubeProjectMetricsHistory{},
&models.SonarqubeProjectAnalysis{},
}
}

Expand All @@ -108,6 +110,11 @@ func (p Sonarqube) SubTaskMetas() []plugin.SubTaskMeta {
tasks.ConvertHotspotsMeta,
tasks.ConvertFileMetricsMeta,
tasks.ConvertAccountsMeta,
tasks.CollectProjectMetricsHistoryMeta,
tasks.ExtractProjectMetricsHistoryMeta,
tasks.CollectProjectAnalysesMeta,
tasks.ExtractProjectAnalysesMeta,
tasks.ConvertProjectMetricsHistoryMeta,
}
}

Expand Down
42 changes: 41 additions & 1 deletion backend/plugins/sonarqube/models/connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"encoding/base64"
"fmt"
"net/http"
"strings"

"github.com/apache/incubator-devlake/core/utils"

Expand All @@ -29,6 +30,12 @@ import (
helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
)

const (
userTokenPrefix = "squ_"
globalAnalysisTokenPrefix = "sqa_"
projectAnalysisTokenPrefix = "sqp_"
)

type SonarqubeAccessToken helper.AccessToken

// SetupAuthentication sets up the HTTP Request Authentication
Expand Down Expand Up @@ -92,7 +99,40 @@ func (connection *SonarqubeConnection) MergeFromRequest(target *SonarqubeConnect
return nil
}

func (connection *SonarqubeConnection) IsCloud() bool {
// ValidateUserTokenPrefix ensures the token is a SonarQube User token on Server
// instances. Global (sqa_) and Project (sqp_) analysis tokens authenticate but
// cannot call read APIs such as measures/component_tree.
func (connection SonarqubeConn) ValidateUserTokenPrefix() errors.Error {
if connection.IsCloud() {
return nil
}
token := strings.TrimSpace(connection.Token)
if token == "" {
return errors.BadInput.New("token is required")
}
switch {
case strings.HasPrefix(token, globalAnalysisTokenPrefix):
return errors.BadInput.New(
"DevLake requires a User token (squ_ prefix). " +
"Global Analysis tokens (sqa_) can push scan results but cannot read project metrics via the Web API. " +
"Create a User token under My Account > Security in SonarQube.",
)
case strings.HasPrefix(token, projectAnalysisTokenPrefix):
return errors.BadInput.New(
"DevLake requires a User token (squ_ prefix). " +
"Project Analysis tokens (sqp_) can push scan results but cannot read project metrics via the Web API. " +
"Create a User token under My Account > Security in SonarQube.",
)
case !strings.HasPrefix(token, userTokenPrefix):
return errors.BadInput.New(
"DevLake requires a User token (squ_ prefix) for SonarQube Server. " +
"Create one under My Account > Security in SonarQube.",
)
}
return nil
}

func (connection SonarqubeConn) IsCloud() bool {
return connection.Endpoint == "https://sonarcloud.io/api/"
}

Expand Down
92 changes: 92 additions & 0 deletions backend/plugins/sonarqube/models/connection_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You 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 models

import (
"testing"

helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
)

func TestValidateUserTokenPrefix(t *testing.T) {
t.Parallel()

serverEndpoint := "https://rad-sonar.example.com/api/"
cloudEndpoint := "https://sonarcloud.io/api/"

tests := []struct {
name string
conn SonarqubeConn
wantErr bool
}{
{
name: "user token on server",
conn: SonarqubeConn{
RestConnection: helper.RestConnection{Endpoint: serverEndpoint},
SonarqubeAccessToken: SonarqubeAccessToken{Token: "squ_abc123"},
},
},
{
name: "global analysis token on server",
conn: SonarqubeConn{
RestConnection: helper.RestConnection{Endpoint: serverEndpoint},
SonarqubeAccessToken: SonarqubeAccessToken{Token: "sqa_abc123"},
},
wantErr: true,
},
{
name: "project analysis token on server",
conn: SonarqubeConn{
RestConnection: helper.RestConnection{Endpoint: serverEndpoint},
SonarqubeAccessToken: SonarqubeAccessToken{Token: "sqp_abc123"},
},
wantErr: true,
},
{
name: "unknown prefix on server",
conn: SonarqubeConn{
RestConnection: helper.RestConnection{Endpoint: serverEndpoint},
SonarqubeAccessToken: SonarqubeAccessToken{Token: "legacy-token"},
},
wantErr: true,
},
{
name: "sonarcloud skips prefix check",
conn: SonarqubeConn{
RestConnection: helper.RestConnection{Endpoint: cloudEndpoint},
SonarqubeAccessToken: SonarqubeAccessToken{Token: "sqa_abc123"},
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := tt.conn.ValidateUserTokenPrefix()
if tt.wantErr {
if err == nil {
t.Fatal("expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
})
}
}
Loading