mirror of
https://github.com/goharbor/harbor.git
synced 2024-09-27 13:02:59 +02:00
add option to store ip addresses and/or user-agents in audit logs
fix ui tests Signed-off-by: Maksym Trofimenko <maksym@container-registry.com>
This commit is contained in:
parent
69fc957d7e
commit
813eb07047
2
Makefile
2
Makefile
@ -468,7 +468,7 @@ misspell:
|
||||
@echo checking misspell...
|
||||
@find . -type d \( -path ./tests \) -prune -o -name '*.go' -print | xargs misspell -error
|
||||
|
||||
# golangci-lint binary installation or refer to https://golangci-lint.run/usage/install/#local-installation
|
||||
# golangci-lint binary installation or refer to https://golangci-lint.run/usage/install/#local-installation
|
||||
# curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.55.2
|
||||
GOLANGCI_LINT := $(shell go env GOPATH)/bin/golangci-lint
|
||||
lint:
|
||||
|
@ -6833,6 +6833,12 @@ definitions:
|
||||
format: date-time
|
||||
example: '2006-01-02T15:04:05Z'
|
||||
description: The time when this operation is triggered.
|
||||
client_ip:
|
||||
type: string
|
||||
description: Client IP address when this operation is triggered.
|
||||
user_agent:
|
||||
type: string
|
||||
description: User agent during the operation.
|
||||
Metadata:
|
||||
type: object
|
||||
properties:
|
||||
@ -7871,6 +7877,16 @@ definitions:
|
||||
x-nullable: true
|
||||
x-omitempty: true
|
||||
$ref: '#/definitions/AuthproxySetting'
|
||||
audit_log_track_ip_address:
|
||||
type: boolean
|
||||
x-nullable: true
|
||||
x-omitempty: true
|
||||
description: The flag to indicate whether IP address tracking is on in audit logs.
|
||||
audit_log_track_user_agent:
|
||||
type: boolean
|
||||
x-nullable: true
|
||||
x-omitempty: true
|
||||
description: The flag to indicate whether user agent tracking is on in audit logs.
|
||||
oidc_provider_name:
|
||||
type: string
|
||||
x-nullable: true
|
||||
@ -8931,6 +8947,12 @@ definitions:
|
||||
skip_audit_log_database:
|
||||
$ref: '#/definitions/BoolConfigItem'
|
||||
description: Whether skip the audit log in database
|
||||
audit_log_track_ip_address:
|
||||
$ref: '#/definitions/BoolConfigItem'
|
||||
description: Whether client ip address tracking is enabled in audit logs
|
||||
audit_log_track_user_agent:
|
||||
$ref: '#/definitions/BoolConfigItem'
|
||||
description: Whether user agent tracking is enabled in audit logs
|
||||
scanner_skip_update_pulltime:
|
||||
$ref: '#/definitions/BoolConfigItem'
|
||||
description: Whether or not to skip update the pull time for scanner
|
||||
@ -9211,6 +9233,16 @@ definitions:
|
||||
description: Skip audit log database
|
||||
x-omitempty: true
|
||||
x-isnullable: true
|
||||
audit_log_track_ip_address:
|
||||
type: boolean
|
||||
description: Track IP addresses in audit logs
|
||||
x-omitempty: true
|
||||
x-isnullable: true
|
||||
audit_log_track_user_agent:
|
||||
type: boolean
|
||||
description: Track user agent in audit logs
|
||||
x-omitempty: true
|
||||
x-isnullable: true
|
||||
session_timeout:
|
||||
type: integer
|
||||
description: The session timeout for harbor, in minutes.
|
||||
|
@ -1,3 +1,6 @@
|
||||
ALTER TABLE audit_log ADD user_agent VARCHAR(255);
|
||||
ALTER TABLE audit_log ADD client_ip inet;
|
||||
|
||||
/*
|
||||
table artifact:
|
||||
id SERIAL PRIMARY KEY NOT NULL,
|
||||
@ -28,4 +31,4 @@ then set column artifact_type as not null
|
||||
*/
|
||||
UPDATE artifact SET artifact_type = media_type;
|
||||
|
||||
ALTER TABLE artifact ALTER COLUMN artifact_type SET NOT NULL;
|
||||
ALTER TABLE artifact ALTER COLUMN artifact_type SET NOT NULL;
|
||||
|
@ -214,6 +214,12 @@ const (
|
||||
AuditLogForwardEndpoint = "audit_log_forward_endpoint"
|
||||
// SkipAuditLogDatabase skip to log audit log in database
|
||||
SkipAuditLogDatabase = "skip_audit_log_database"
|
||||
|
||||
// AuditLogTrackIPAddress track client ip address with audit_logs
|
||||
AuditLogTrackIPAddress = "audit_log_track_ip_address"
|
||||
// AuditLogTrackUserAgent track user agent with audit_logs
|
||||
AuditLogTrackUserAgent = "audit_log_track_user_agent"
|
||||
|
||||
// MaxAuditRetentionHour allowed in audit log purge
|
||||
MaxAuditRetentionHour = 240000
|
||||
// ScannerSkipUpdatePullTime
|
||||
|
@ -18,6 +18,7 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/goharbor/harbor/src/controller/event"
|
||||
"github.com/goharbor/harbor/src/lib"
|
||||
"github.com/goharbor/harbor/src/lib/config"
|
||||
"github.com/goharbor/harbor/src/lib/log"
|
||||
"github.com/goharbor/harbor/src/pkg/audit"
|
||||
@ -40,7 +41,6 @@ func (h *Handler) Name() string {
|
||||
|
||||
// Handle ...
|
||||
func (h *Handler) Handle(ctx context.Context, value interface{}) error {
|
||||
var auditLog *am.AuditLog
|
||||
var addAuditLog bool
|
||||
switch v := value.(type) {
|
||||
case *event.PushArtifactEvent, *event.DeleteArtifactEvent,
|
||||
@ -60,9 +60,17 @@ func (h *Handler) Handle(ctx context.Context, value interface{}) error {
|
||||
log.Errorf("failed to handler event %v", err)
|
||||
return err
|
||||
}
|
||||
auditLog = al
|
||||
if auditLog != nil {
|
||||
_, err := audit.Mgr.Create(ctx, auditLog)
|
||||
|
||||
if al != nil {
|
||||
ip := lib.GetClientIPAddress(ctx)
|
||||
if ip != "" {
|
||||
al.ClientIP = &ip
|
||||
}
|
||||
ua := lib.GetUserAgent(ctx)
|
||||
if ua != "" {
|
||||
al.UserAgent = &ua
|
||||
}
|
||||
_, err := audit.Mgr.Create(ctx, al)
|
||||
if err != nil {
|
||||
log.Debugf("add audit log err: %v", err)
|
||||
}
|
||||
|
@ -49,10 +49,16 @@ type Data struct {
|
||||
HarborVersion string
|
||||
BannerMessage string
|
||||
AuthProxySettings *models.HTTPAuthProxy
|
||||
AuditLogs AuditLogSettings
|
||||
Protected *protectedData
|
||||
OIDCProviderName string
|
||||
}
|
||||
|
||||
type AuditLogSettings struct {
|
||||
TrackIPAddress bool
|
||||
TrackUserAgent bool
|
||||
}
|
||||
|
||||
type protectedData struct {
|
||||
CurrentTime time.Time
|
||||
RegistryURL string
|
||||
@ -105,6 +111,10 @@ func (c *controller) GetInfo(ctx context.Context, opt Options) (*Data, error) {
|
||||
HarborVersion: fmt.Sprintf("%s-%s", version.ReleaseVersion, version.GitCommit),
|
||||
BannerMessage: utils.SafeCastString(mgr.Get(ctx, common.BannerMessage).GetString()),
|
||||
OIDCProviderName: OIDCProviderName(cfg),
|
||||
AuditLogs: AuditLogSettings{
|
||||
TrackIPAddress: utils.SafeCastBool(cfg[common.AuditLogTrackIPAddress]),
|
||||
TrackUserAgent: utils.SafeCastBool(cfg[common.AuditLogTrackUserAgent]),
|
||||
},
|
||||
}
|
||||
if res.AuthMode == common.HTTPAuth {
|
||||
if s, err := config.HTTPAuthProxySetting(ctx); err == nil {
|
||||
|
@ -18,6 +18,8 @@ import (
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
"github.com/goharbor/harbor/src/server/middleware/clientinfo"
|
||||
|
||||
"github.com/beego/beego/v2/server/web"
|
||||
|
||||
"github.com/goharbor/harbor/src/pkg/distribution"
|
||||
@ -94,6 +96,7 @@ func MiddleWares() []web.MiddleWare {
|
||||
session.Middleware(),
|
||||
csrf.Middleware(),
|
||||
orm.Middleware(pingSkipper),
|
||||
clientinfo.Middleware(pingSkipper),
|
||||
notification.Middleware(pingSkipper), // notification must ahead of transaction ensure the DB transaction execution complete
|
||||
transaction.Middleware(dbTxSkippers...),
|
||||
artifactinfo.Middleware(),
|
||||
|
@ -78,7 +78,7 @@ func GetManager(name string) (Manager, error) {
|
||||
func DefaultMgr() Manager {
|
||||
manager, err := GetManager(DefaultCfgManager)
|
||||
if err != nil {
|
||||
log.Error("failed to get config manager")
|
||||
log.Error("failed to get config manager", err)
|
||||
}
|
||||
return manager
|
||||
}
|
||||
|
@ -189,6 +189,9 @@ var (
|
||||
|
||||
{Name: common.AuditLogForwardEndpoint, Scope: UserScope, Group: BasicGroup, EnvKey: "AUDIT_LOG_FORWARD_ENDPOINT", DefaultValue: "", ItemType: &StringType{}, Editable: false, Description: `The endpoint to forward the audit log.`},
|
||||
{Name: common.SkipAuditLogDatabase, Scope: UserScope, Group: BasicGroup, EnvKey: "SKIP_LOG_AUDIT_DATABASE", DefaultValue: "false", ItemType: &BoolType{}, Editable: false, Description: `The option to skip audit log in database`},
|
||||
{Name: common.AuditLogTrackIPAddress, Scope: UserScope, Group: BasicGroup, EnvKey: "AUDIT_LOG_TRACK_IP_ADDRESS", DefaultValue: "false", ItemType: &BoolType{}, Editable: true, Description: `The flag to enable IP addresses tracking in audit logs.`},
|
||||
{Name: common.AuditLogTrackUserAgent, Scope: UserScope, Group: BasicGroup, EnvKey: "AUDIT_LOG_TRACK_USER_AGENT", DefaultValue: "false", ItemType: &BoolType{}, Editable: true, Description: `The flag to enable user agent tracking in audit logs.`},
|
||||
|
||||
{Name: common.ScannerSkipUpdatePullTime, Scope: UserScope, Group: BasicGroup, EnvKey: "SCANNER_SKIP_UPDATE_PULL_TIME", DefaultValue: "false", ItemType: &BoolType{}, Editable: false, Description: `The option to skip update pull time for scanner`},
|
||||
|
||||
{Name: common.SessionTimeout, Scope: UserScope, Group: BasicGroup, EnvKey: "SESSION_TIMEOUT", DefaultValue: "60", ItemType: &Int64Type{}, Editable: true, Description: `The session timeout in minutes`},
|
||||
|
@ -251,6 +251,16 @@ func SkipAuditLogDatabase(ctx context.Context) bool {
|
||||
return DefaultMgr().Get(ctx, common.SkipAuditLogDatabase).GetBool()
|
||||
}
|
||||
|
||||
// AuditLogTrackIPAddress enables ip address tracking
|
||||
func AuditLogTrackIPAddress(ctx context.Context) bool {
|
||||
return DefaultMgr().Get(ctx, common.AuditLogTrackIPAddress).GetBool()
|
||||
}
|
||||
|
||||
// AuditLogTrackUserAgent enables user info tracking
|
||||
func AuditLogTrackUserAgent(ctx context.Context) bool {
|
||||
return DefaultMgr().Get(ctx, common.AuditLogTrackUserAgent).GetBool()
|
||||
}
|
||||
|
||||
// ScannerSkipUpdatePullTime returns the scanner skip update pull time setting
|
||||
func ScannerSkipUpdatePullTime(ctx context.Context) bool {
|
||||
return DefaultMgr().Get(ctx, common.ScannerSkipUpdatePullTime).GetBool()
|
||||
|
@ -27,6 +27,8 @@ const (
|
||||
contextKeyAuthMode contextKey = "authMode"
|
||||
contextKeyCarrySession contextKey = "carrySession"
|
||||
contextKeyRequestID contextKey = "X-Request-ID"
|
||||
contextClientIPAddress contextKey = "clientIPAddress"
|
||||
contextUserAgent contextKey = "userAgent"
|
||||
)
|
||||
|
||||
// ArtifactInfo wraps the artifact info extracted from the request to "/v2/"
|
||||
@ -128,3 +130,33 @@ func GetXRequestID(ctx context.Context) string {
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
// WithClientIPAddress returns a context with ipAddress set
|
||||
func WithClientIPAddress(ctx context.Context, ipAddress string) context.Context {
|
||||
return setToContext(ctx, contextClientIPAddress, ipAddress)
|
||||
}
|
||||
|
||||
// WithUserAgent returns a context with user agent set
|
||||
func WithUserAgent(ctx context.Context, userAgent string) context.Context {
|
||||
return setToContext(ctx, contextUserAgent, userAgent)
|
||||
}
|
||||
|
||||
// GetClientIPAddress gets the ip address from the context
|
||||
func GetClientIPAddress(ctx context.Context) string {
|
||||
var result string
|
||||
value := getFromContext(ctx, contextClientIPAddress)
|
||||
if value != nil {
|
||||
result, _ = value.(string)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// GetUserAgent gets the user agent from the context
|
||||
func GetUserAgent(ctx context.Context) string {
|
||||
var result string
|
||||
value := getFromContext(ctx, contextUserAgent)
|
||||
if value != nil {
|
||||
result, _ = value.(string)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
@ -77,9 +77,15 @@ func (m *manager) Get(ctx context.Context, id int64) (*model.AuditLog, error) {
|
||||
// Create ...
|
||||
func (m *manager) Create(ctx context.Context, audit *model.AuditLog) (int64, error) {
|
||||
if len(config.AuditLogForwardEndpoint(ctx)) > 0 {
|
||||
LogMgr.DefaultLogger(ctx).WithField("operator", audit.Username).
|
||||
WithField("time", audit.OpTime).WithField("resourceType", audit.ResourceType).
|
||||
Infof("action:%s, resource:%s", audit.Operation, audit.Resource)
|
||||
logger := LogMgr.DefaultLogger(ctx).WithField("operator", audit.Username).
|
||||
WithField("time", audit.OpTime).WithField("resourceType", audit.ResourceType)
|
||||
if config.AuditLogTrackIPAddress(ctx) && audit.ClientIP != nil {
|
||||
logger.WithField("clientIP", *audit.ClientIP)
|
||||
}
|
||||
if config.AuditLogTrackUserAgent(ctx) && audit.UserAgent != nil {
|
||||
logger.WithField("userAgent", *audit.UserAgent)
|
||||
}
|
||||
logger.Infof("action:%s, resource:%s", audit.Operation, audit.Resource)
|
||||
}
|
||||
if config.SkipAuditLogDatabase(ctx) {
|
||||
return 0, nil
|
||||
|
@ -33,6 +33,8 @@ type AuditLog struct {
|
||||
Resource string `orm:"column(resource)" json:"resource"`
|
||||
Username string `orm:"column(username)" json:"username"`
|
||||
OpTime time.Time `orm:"column(op_time)" json:"op_time" sort:"default:desc"`
|
||||
UserAgent *string `orm:"column(user_agent)" json:"user_agent"`
|
||||
ClientIP *string `orm:"column(client_ip)" json:"client_ip"`
|
||||
}
|
||||
|
||||
// TableName for audit log
|
||||
|
@ -112,6 +112,8 @@ export class Configuration {
|
||||
oidc_group_filter: StringValueItem;
|
||||
audit_log_forward_endpoint: StringValueItem;
|
||||
skip_audit_log_database: BoolValueItem;
|
||||
audit_log_track_ip_address: BoolValueItem;
|
||||
audit_log_track_user_agent: BoolValueItem;
|
||||
session_timeout: NumberValueItem;
|
||||
scanner_skip_update_pulltime: BoolValueItem;
|
||||
banner_message: StringValueItem;
|
||||
@ -189,6 +191,8 @@ export class Configuration {
|
||||
this.storage_per_project = new NumberValueItem(-1, true);
|
||||
this.audit_log_forward_endpoint = new StringValueItem('', true);
|
||||
this.skip_audit_log_database = new BoolValueItem(false, true);
|
||||
this.audit_log_track_ip_address = new BoolValueItem(false, true);
|
||||
this.audit_log_track_user_agent = new BoolValueItem(false, true);
|
||||
this.session_timeout = new NumberValueItem(60, true);
|
||||
this.scanner_skip_update_pulltime = new BoolValueItem(false, true);
|
||||
this.banner_message = new StringValueItem(
|
||||
|
@ -346,6 +346,64 @@
|
||||
" />
|
||||
</clr-checkbox-wrapper>
|
||||
</clr-checkbox-container>
|
||||
<clr-checkbox-container class="center">
|
||||
<label for="auditLogTrackIpAddress"
|
||||
>{{ 'AUDIT_LOG.TRACK_IP' | translate }}
|
||||
<clr-tooltip>
|
||||
<clr-icon
|
||||
clrTooltipTrigger
|
||||
shape="info-circle"
|
||||
size="24"></clr-icon>
|
||||
<clr-tooltip-content
|
||||
clrPosition="top-right"
|
||||
clrSize="lg"
|
||||
*clrIfOpen>
|
||||
<span>{{
|
||||
'AUDIT_LOG.TRACK_IP_TOOLTIP' | translate
|
||||
}}</span>
|
||||
</clr-tooltip-content>
|
||||
</clr-tooltip>
|
||||
</label>
|
||||
<clr-checkbox-wrapper>
|
||||
<input
|
||||
type="checkbox"
|
||||
clrCheckbox
|
||||
name="auditLogTrackIpAddress"
|
||||
id="auditLogTrackIpAddress"
|
||||
[(ngModel)]="
|
||||
currentConfig.audit_log_track_ip_address.value
|
||||
" />
|
||||
</clr-checkbox-wrapper>
|
||||
</clr-checkbox-container>
|
||||
<clr-checkbox-container class="center">
|
||||
<label for="auditLogTrackUserAgent"
|
||||
>{{ 'AUDIT_LOG.TRACK_UA' | translate }}
|
||||
<clr-tooltip>
|
||||
<clr-icon
|
||||
clrTooltipTrigger
|
||||
shape="info-circle"
|
||||
size="24"></clr-icon>
|
||||
<clr-tooltip-content
|
||||
clrPosition="top-right"
|
||||
clrSize="lg"
|
||||
*clrIfOpen>
|
||||
<span>{{
|
||||
'AUDIT_LOG.TRACK_UA_TOOLTIP' | translate
|
||||
}}</span>
|
||||
</clr-tooltip-content>
|
||||
</clr-tooltip>
|
||||
</label>
|
||||
<clr-checkbox-wrapper>
|
||||
<input
|
||||
type="checkbox"
|
||||
clrCheckbox
|
||||
name="auditLogTrackUserAgent"
|
||||
id="auditLogTrackUserAgent"
|
||||
[(ngModel)]="
|
||||
currentConfig.audit_log_track_user_agent.value
|
||||
" />
|
||||
</clr-checkbox-wrapper>
|
||||
</clr-checkbox-container>
|
||||
<div class="clr-form-control">
|
||||
<label class="clr-control-label">{{
|
||||
'BANNER_MESSAGE.BANNER_MESSAGE' | translate
|
||||
|
@ -188,6 +188,8 @@ export class SystemSettingsComponent implements OnInit, OnDestroy {
|
||||
prop === 'robot_name_prefix' ||
|
||||
prop === 'audit_log_forward_endpoint' ||
|
||||
prop === 'skip_audit_log_database' ||
|
||||
prop === 'audit_log_track_ip_address' ||
|
||||
prop === 'audit_log_track_user_agent' ||
|
||||
prop === 'session_timeout' ||
|
||||
prop === 'scanner_skip_update_pulltime' ||
|
||||
prop === 'banner_message'
|
||||
|
@ -210,6 +210,14 @@ describe('ProjectComponent', () => {
|
||||
value: false,
|
||||
editable: true,
|
||||
},
|
||||
audit_log_track_ip_address: {
|
||||
value: false,
|
||||
editable: true,
|
||||
},
|
||||
audit_log_track_user_agent: {
|
||||
value: false,
|
||||
editable: true,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
|
@ -69,6 +69,12 @@
|
||||
<clr-dg-column>{{
|
||||
'AUDIT_LOG.OPERATION' | translate
|
||||
}}</clr-dg-column>
|
||||
<clr-dg-column *ngIf="isIPTracked()">{{
|
||||
'AUDIT_LOG.IP_ADDRESS' | translate
|
||||
}}</clr-dg-column>
|
||||
<clr-dg-column *ngIf="isUATracked()">{{
|
||||
'AUDIT_LOG.USER_AGENT' | translate
|
||||
}}</clr-dg-column>
|
||||
<clr-dg-column>{{
|
||||
'AUDIT_LOG.TIMESTAMP' | translate
|
||||
}}</clr-dg-column>
|
||||
@ -77,6 +83,12 @@
|
||||
<clr-dg-cell>{{ l.resource }}</clr-dg-cell>
|
||||
<clr-dg-cell>{{ l.resource_type }}</clr-dg-cell>
|
||||
<clr-dg-cell>{{ l.operation }}</clr-dg-cell>
|
||||
<clr-dg-cell *ngIf="isIPTracked()">{{
|
||||
l.client_ip
|
||||
}}</clr-dg-cell>
|
||||
<clr-dg-cell *ngIf="isUATracked()">{{
|
||||
l.user_agent
|
||||
}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{
|
||||
l.op_time | harborDatetime : 'short'
|
||||
}}</clr-dg-cell>
|
||||
|
@ -13,6 +13,7 @@ import { SharedTestingModule } from '../../../shared/shared.module';
|
||||
import { registerLocaleData } from '@angular/common';
|
||||
import locale_en from '@angular/common/locales/en';
|
||||
import { DatePickerComponent } from '../../../shared/components/datetime-picker/datetime-picker.component';
|
||||
import { AppConfigService } from '../../../services/app-config.service';
|
||||
|
||||
describe('AuditLogComponent', () => {
|
||||
let component: AuditLogComponent;
|
||||
@ -20,6 +21,9 @@ describe('AuditLogComponent', () => {
|
||||
const mockMessageHandlerService = {
|
||||
handleError: () => {},
|
||||
};
|
||||
let fakeAppConfigService = {
|
||||
handleError: () => {},
|
||||
};
|
||||
const mockActivatedRoute = {
|
||||
parent: {
|
||||
parent: {
|
||||
@ -105,6 +109,7 @@ describe('AuditLogComponent', () => {
|
||||
provide: MessageHandlerService,
|
||||
useValue: mockMessageHandlerService,
|
||||
},
|
||||
{ provide: AppConfigService, useValue: fakeAppConfigService },
|
||||
],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
@ -25,6 +25,7 @@ import {
|
||||
setPageSizeToLocalStorage,
|
||||
} from '../../../shared/units/utils';
|
||||
import { ClrDatagridStateInterface } from '@clr/angular';
|
||||
import { AppConfigService } from '../../../services/app-config.service';
|
||||
|
||||
const optionalSearch: {} = { 0: 'AUDIT_LOG.ADVANCED', 1: 'AUDIT_LOG.SIMPLE' };
|
||||
|
||||
@ -104,7 +105,8 @@ export class AuditLogComponent implements OnInit {
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private auditLogService: ProjectService,
|
||||
private messageHandlerService: MessageHandlerService
|
||||
private messageHandlerService: MessageHandlerService,
|
||||
private appConfigService: AppConfigService
|
||||
) {
|
||||
// Get current user from registered resolver.
|
||||
this.route.data.subscribe(
|
||||
@ -120,6 +122,19 @@ export class AuditLogComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
isIPTracked(): boolean {
|
||||
if (this.appConfigService?.configurations?.audit_log_track_ip_address) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
isUATracked(): boolean {
|
||||
if (this.appConfigService?.configurations?.audit_log_track_user_agent) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
retrieve(state?: ClrDatagridStateInterface) {
|
||||
if (state && state.page) {
|
||||
this.pageSize = state.page.size;
|
||||
|
@ -19,6 +19,8 @@ export class AppConfig {
|
||||
admiral_endpoint: string;
|
||||
auth_mode: string;
|
||||
primary_auth_mode: boolean;
|
||||
audit_log_track_ip_address: boolean;
|
||||
audit_log_track_user_agent: boolean;
|
||||
registry_url: string;
|
||||
project_creation_restriction: string;
|
||||
self_registration: boolean;
|
||||
|
@ -534,7 +534,13 @@
|
||||
"OF": "of",
|
||||
"NOT_FOUND": "We couldn't find any logs!",
|
||||
"RESOURCE": "Resource",
|
||||
"RESOURCE_TYPE": "Resource Type"
|
||||
"RESOURCE_TYPE": "Resource Type",
|
||||
"IP_ADDRESS": "IP Address",
|
||||
"USER_AGENT": "User Agent",
|
||||
"TRACK_IP": "IP-Address in Audit Log",
|
||||
"TRACK_IP_TOOLTIP": "Capture the client's IP addresses on each request and store them in the audit log.",
|
||||
"TRACK_UA": "User-Agent in Audit Log",
|
||||
"TRACK_UA_TOOLTIP": "Capture the client's User-Agent on each request and store it in the audit log."
|
||||
},
|
||||
"REPLICATION": {
|
||||
"PUSH_BASED_ONLY": "Only for the push-based replication",
|
||||
|
86
src/server/middleware/clientinfo/clientinfo.go
Normal file
86
src/server/middleware/clientinfo/clientinfo.go
Normal file
@ -0,0 +1,86 @@
|
||||
// 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 clientinfo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/goharbor/harbor/src/lib/config"
|
||||
|
||||
"github.com/goharbor/harbor/src/lib"
|
||||
"github.com/goharbor/harbor/src/server/middleware"
|
||||
)
|
||||
|
||||
var (
|
||||
// De-facto standard header keys.
|
||||
xForwardedFor = http.CanonicalHeaderKey("X-Forwarded-For")
|
||||
xRealIP = http.CanonicalHeaderKey("X-Real-IP")
|
||||
|
||||
// RFC7239 defines a new "Forwarded: " header designed to replace the
|
||||
// existing use of X-Forwarded-* headers.
|
||||
// e.g. Forwarded: for=192.0.2.60;proto=https;by=203.0.113.43
|
||||
forwarded = http.CanonicalHeaderKey("Forwarded")
|
||||
// Allows for a sub-match of the first value after 'for=' to the next
|
||||
// comma, semi-colon or space. The match is case-insensitive.
|
||||
forRegex = regexp.MustCompile(`(?i)(?:for=)([^(;|,| )]+)`)
|
||||
)
|
||||
|
||||
func getIP(r *http.Request) string {
|
||||
var addr string
|
||||
|
||||
if fwd := r.Header.Get(xForwardedFor); fwd != "" {
|
||||
// Only grab the first (client) address. Note that '192.168.0.1,
|
||||
// 10.1.1.1' is a valid key for X-Forwarded-For where addresses after
|
||||
// the first may represent forwarding proxies earlier in the chain.
|
||||
s := strings.Index(fwd, ", ")
|
||||
if s == -1 {
|
||||
s = len(fwd)
|
||||
}
|
||||
addr = fwd[:s]
|
||||
} else if fwd := r.Header.Get(xRealIP); fwd != "" {
|
||||
// X-Real-IP should only contain one IP address (the client making the
|
||||
// request).
|
||||
addr = fwd
|
||||
} else if fwd := r.Header.Get(forwarded); fwd != "" {
|
||||
// match should contain at least two elements if the protocol was
|
||||
// specified in the Forwarded header. The first element will always be
|
||||
// the 'for=' capture, which we ignore. In the case of multiple IP
|
||||
// addresses (for=8.8.8.8, 8.8.4.4,172.16.1.20 is valid) we only
|
||||
// extract the first, which should be the client IP.
|
||||
if match := forRegex.FindStringSubmatch(fwd); len(match) > 1 {
|
||||
// IPv6 addresses in Forwarded headers are quoted-strings. We strip
|
||||
// these quotes.
|
||||
addr = strings.Trim(match[1], `"`)
|
||||
}
|
||||
}
|
||||
|
||||
return addr
|
||||
}
|
||||
|
||||
// Middleware sends the notification after transaction success
|
||||
func Middleware(skippers ...middleware.Skipper) func(http.Handler) http.Handler {
|
||||
return middleware.New(func(w http.ResponseWriter, r *http.Request, next http.Handler) {
|
||||
ctx := r.Context()
|
||||
if config.AuditLogTrackIPAddress(ctx) {
|
||||
ctx = lib.WithClientIPAddress(ctx, getIP(r))
|
||||
}
|
||||
if config.AuditLogTrackUserAgent(ctx) {
|
||||
ctx = lib.WithUserAgent(ctx, r.UserAgent())
|
||||
}
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
}, skippers...)
|
||||
}
|
152
src/server/middleware/clientinfo/clientinfo_test.go
Normal file
152
src/server/middleware/clientinfo/clientinfo_test.go
Normal file
@ -0,0 +1,152 @@
|
||||
package clientinfo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/utils/test"
|
||||
"github.com/goharbor/harbor/src/lib"
|
||||
"github.com/goharbor/harbor/src/lib/config"
|
||||
_ "github.com/goharbor/harbor/src/pkg/config/inmemory"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMiddleware(t *testing.T) {
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
init func()
|
||||
teardown func()
|
||||
buildReq func() *http.Request
|
||||
checkReq func(t *testing.T, r *http.Request)
|
||||
}{
|
||||
{
|
||||
name: "test ip track on using X-Real-IP",
|
||||
buildReq: func() *http.Request {
|
||||
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
r.Header.Set("X-Real-IP", "192.168.0.1")
|
||||
return r
|
||||
},
|
||||
checkReq: func(t *testing.T, r *http.Request) {
|
||||
assert.Equal(t, "192.168.0.1", lib.GetClientIPAddress(r.Context()))
|
||||
},
|
||||
init: func() {
|
||||
config.InitWithSettings(map[string]interface{}{
|
||||
common.AuditLogTrackIPAddress: "true",
|
||||
})
|
||||
},
|
||||
teardown: func() {
|
||||
config.InitWithSettings(test.GetUnitTestConfig())
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "test ip track on using X-Forwarded-For",
|
||||
buildReq: func() *http.Request {
|
||||
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
r.Header.Set("X-Forwarded-For", "192.168.0.1")
|
||||
return r
|
||||
},
|
||||
checkReq: func(t *testing.T, r *http.Request) {
|
||||
assert.Equal(t, "192.168.0.1", lib.GetClientIPAddress(r.Context()))
|
||||
},
|
||||
init: func() {
|
||||
config.InitWithSettings(map[string]interface{}{
|
||||
common.AuditLogTrackIPAddress: "true",
|
||||
})
|
||||
},
|
||||
teardown: func() {
|
||||
config.InitWithSettings(test.GetUnitTestConfig())
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "test ip track on, X-Forwarded-For superiors X-Real-IP",
|
||||
buildReq: func() *http.Request {
|
||||
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
r.Header.Set("X-Real-IP", "192.168.0.10")
|
||||
r.Header.Set("X-Forwarded-For", "192.168.0.1")
|
||||
return r
|
||||
},
|
||||
checkReq: func(t *testing.T, r *http.Request) {
|
||||
assert.Equal(t, "192.168.0.1", lib.GetClientIPAddress(r.Context()))
|
||||
},
|
||||
init: func() {
|
||||
config.InitWithSettings(map[string]interface{}{
|
||||
common.AuditLogTrackIPAddress: "true",
|
||||
})
|
||||
},
|
||||
teardown: func() {
|
||||
config.InitWithSettings(test.GetUnitTestConfig())
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "test ip track off using X-Real-IP",
|
||||
buildReq: func() *http.Request {
|
||||
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
r.Header.Set("X-Real-IP", "192.168.0.1")
|
||||
return r
|
||||
},
|
||||
checkReq: func(t *testing.T, r *http.Request) {
|
||||
assert.Equal(t, "", lib.GetClientIPAddress(r.Context()))
|
||||
},
|
||||
init: func() {
|
||||
config.InitWithSettings(map[string]interface{}{
|
||||
common.AuditLogTrackIPAddress: "false",
|
||||
})
|
||||
},
|
||||
teardown: func() {
|
||||
config.InitWithSettings(test.GetUnitTestConfig())
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "test user agent track on",
|
||||
buildReq: func() *http.Request {
|
||||
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
r.Header.Set("User-Agent", "harbor-test")
|
||||
return r
|
||||
},
|
||||
checkReq: func(t *testing.T, r *http.Request) {
|
||||
assert.Equal(t, "harbor-test", lib.GetUserAgent(r.Context()))
|
||||
},
|
||||
init: func() {
|
||||
config.InitWithSettings(map[string]interface{}{
|
||||
common.AuditLogTrackUserAgent: "true",
|
||||
})
|
||||
},
|
||||
teardown: func() {
|
||||
config.InitWithSettings(test.GetUnitTestConfig())
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "test user agent track off",
|
||||
buildReq: func() *http.Request {
|
||||
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
r.Header.Set("User-Agent", "harbor-test")
|
||||
return r
|
||||
},
|
||||
checkReq: func(t *testing.T, r *http.Request) {
|
||||
assert.Equal(t, "", lib.GetUserAgent(r.Context()))
|
||||
},
|
||||
init: func() {
|
||||
config.InitWithSettings(map[string]interface{}{
|
||||
common.AuditLogTrackUserAgent: "false",
|
||||
})
|
||||
},
|
||||
teardown: func() {
|
||||
config.InitWithSettings(test.GetUnitTestConfig())
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
var r *http.Request
|
||||
c.init()
|
||||
Middleware()(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
r = req
|
||||
})).ServeHTTP(httptest.NewRecorder(), c.buildReq())
|
||||
c.teardown()
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
c.checkReq(t, r)
|
||||
})
|
||||
}
|
||||
}
|
@ -96,14 +96,21 @@ func (a *auditlogAPI) ListAuditLogs(ctx context.Context, params auditlog.ListAud
|
||||
|
||||
var auditLogs []*models.AuditLog
|
||||
for _, log := range logs {
|
||||
auditLogs = append(auditLogs, &models.AuditLog{
|
||||
al := &models.AuditLog{
|
||||
ID: log.ID,
|
||||
Resource: log.Resource,
|
||||
ResourceType: log.ResourceType,
|
||||
Username: log.Username,
|
||||
Operation: log.Operation,
|
||||
OpTime: strfmt.DateTime(log.OpTime),
|
||||
})
|
||||
}
|
||||
if log.UserAgent != nil {
|
||||
al.UserAgent = *log.UserAgent
|
||||
}
|
||||
if log.ClientIP != nil {
|
||||
al.ClientIP = *log.ClientIP
|
||||
}
|
||||
auditLogs = append(auditLogs, al)
|
||||
}
|
||||
return auditlog.NewListAuditLogsOK().
|
||||
WithXTotalCount(total).
|
||||
|
@ -315,14 +315,21 @@ func (a *projectAPI) GetLogs(ctx context.Context, params operation.GetLogsParams
|
||||
|
||||
var auditLogs []*models.AuditLog
|
||||
for _, log := range logs {
|
||||
auditLogs = append(auditLogs, &models.AuditLog{
|
||||
al := &models.AuditLog{
|
||||
ID: log.ID,
|
||||
Resource: log.Resource,
|
||||
ResourceType: log.ResourceType,
|
||||
Username: log.Username,
|
||||
Operation: log.Operation,
|
||||
OpTime: strfmt.DateTime(log.OpTime),
|
||||
})
|
||||
}
|
||||
if log.UserAgent != nil {
|
||||
al.UserAgent = *log.UserAgent
|
||||
}
|
||||
if log.ClientIP != nil {
|
||||
al.ClientIP = *log.ClientIP
|
||||
}
|
||||
auditLogs = append(auditLogs, al)
|
||||
}
|
||||
return operation.NewGetLogsOK().
|
||||
WithXTotalCount(total).
|
||||
|
@ -82,12 +82,14 @@ func (s *sysInfoAPI) convertInfo(d *si.Data) *models.GeneralInfo {
|
||||
return nil
|
||||
}
|
||||
res := &models.GeneralInfo{
|
||||
AuthMode: &d.AuthMode,
|
||||
PrimaryAuthMode: &d.PrimaryAuthMode,
|
||||
SelfRegistration: &d.SelfRegistration,
|
||||
HarborVersion: &d.HarborVersion,
|
||||
BannerMessage: &d.BannerMessage,
|
||||
OIDCProviderName: &d.OIDCProviderName,
|
||||
AuthMode: &d.AuthMode,
|
||||
PrimaryAuthMode: &d.PrimaryAuthMode,
|
||||
SelfRegistration: &d.SelfRegistration,
|
||||
HarborVersion: &d.HarborVersion,
|
||||
BannerMessage: &d.BannerMessage,
|
||||
OIDCProviderName: &d.OIDCProviderName,
|
||||
AuditLogTrackIPAddress: &d.AuditLogs.TrackIPAddress,
|
||||
AuditLogTrackUserAgent: &d.AuditLogs.TrackUserAgent,
|
||||
}
|
||||
if d.AuthProxySettings != nil {
|
||||
res.AuthproxySettings = &models.AuthproxySetting{
|
||||
|
Loading…
Reference in New Issue
Block a user