mirror of
https://github.com/coder/coder.git
synced 2025-07-13 21:36:50 +00:00
feat: add organization details to audit log response (#13961)
* Allow creating test audits with nil org Not all audit entries have organization IDs, so this will allow us to test those cases. * Add organization details to audit log queries * Add organization to audit log response This replaces the old ID. This is a breaking change but organizations were not being used before.
This commit is contained in:
25
coderd/apidoc/docs.go
generated
25
coderd/apidoc/docs.go
generated
@ -8368,7 +8368,11 @@ const docTemplate = `{
|
||||
"is_deleted": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"organization": {
|
||||
"$ref": "#/definitions/codersdk.MinimalOrganization"
|
||||
},
|
||||
"organization_id": {
|
||||
"description": "Deprecated: Use 'organization.id' instead.",
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
@ -10102,6 +10106,27 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.MinimalOrganization": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id"
|
||||
],
|
||||
"properties": {
|
||||
"display_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"icon": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.MinimalUser": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
23
coderd/apidoc/swagger.json
generated
23
coderd/apidoc/swagger.json
generated
@ -7434,7 +7434,11 @@
|
||||
"is_deleted": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"organization": {
|
||||
"$ref": "#/definitions/codersdk.MinimalOrganization"
|
||||
},
|
||||
"organization_id": {
|
||||
"description": "Deprecated: Use 'organization.id' instead.",
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
@ -9054,6 +9058,25 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.MinimalOrganization": {
|
||||
"type": "object",
|
||||
"required": ["id"],
|
||||
"properties": {
|
||||
"display_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"icon": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.MinimalUser": {
|
||||
"type": "object",
|
||||
"required": ["id", "username"],
|
||||
|
@ -145,9 +145,6 @@ func (api *API) generateFakeAuditLog(rw http.ResponseWriter, r *http.Request) {
|
||||
if len(params.AdditionalFields) == 0 {
|
||||
params.AdditionalFields = json.RawMessage("{}")
|
||||
}
|
||||
if params.OrganizationID == uuid.Nil {
|
||||
params.OrganizationID = uuid.New()
|
||||
}
|
||||
|
||||
_, err = api.Database.InsertAuditLog(ctx, database.InsertAuditLogParams{
|
||||
ID: uuid.New(),
|
||||
@ -241,10 +238,11 @@ func (api *API) convertAuditLog(ctx context.Context, dblog database.GetAuditLogs
|
||||
resourceLink = api.auditLogResourceLink(ctx, dblog, additionalFields)
|
||||
}
|
||||
|
||||
return codersdk.AuditLog{
|
||||
ID: dblog.ID,
|
||||
RequestID: dblog.RequestID,
|
||||
Time: dblog.Time,
|
||||
alog := codersdk.AuditLog{
|
||||
ID: dblog.ID,
|
||||
RequestID: dblog.RequestID,
|
||||
Time: dblog.Time,
|
||||
// OrganizationID is deprecated.
|
||||
OrganizationID: dblog.OrganizationID,
|
||||
IP: ip,
|
||||
UserAgent: dblog.UserAgent.String,
|
||||
@ -261,6 +259,17 @@ func (api *API) convertAuditLog(ctx context.Context, dblog database.GetAuditLogs
|
||||
ResourceLink: resourceLink,
|
||||
IsDeleted: isDeleted,
|
||||
}
|
||||
|
||||
if dblog.OrganizationID != uuid.Nil {
|
||||
alog.Organization = &codersdk.MinimalOrganization{
|
||||
ID: dblog.OrganizationID,
|
||||
Name: dblog.OrganizationName,
|
||||
DisplayName: dblog.OrganizationDisplayName,
|
||||
Icon: dblog.OrganizationIcon,
|
||||
}
|
||||
}
|
||||
|
||||
return alog
|
||||
}
|
||||
|
||||
func auditLogDescription(alog database.GetAuditLogsOffsetRow) string {
|
||||
|
@ -46,7 +46,7 @@ func TestAuditLogs(t *testing.T) {
|
||||
require.Len(t, alogs.AuditLogs, 1)
|
||||
})
|
||||
|
||||
t.Run("User", func(t *testing.T) {
|
||||
t.Run("IncludeUser", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
@ -95,6 +95,92 @@ func TestAuditLogs(t *testing.T) {
|
||||
require.Equal(t, foundUser, *alogs.AuditLogs[0].User)
|
||||
})
|
||||
|
||||
t.Run("IncludeOrganization", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
|
||||
o, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{
|
||||
Name: "new-org",
|
||||
DisplayName: "New organization",
|
||||
Description: "A new organization to love and cherish until the test is over.",
|
||||
Icon: "/emojis/1f48f-1f3ff.png",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{
|
||||
OrganizationID: o.ID,
|
||||
ResourceID: user.UserID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
alogs, err := client.AuditLogs(ctx, codersdk.AuditLogsRequest{
|
||||
Pagination: codersdk.Pagination{
|
||||
Limit: 1,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(1), alogs.Count)
|
||||
require.Len(t, alogs.AuditLogs, 1)
|
||||
|
||||
// Make sure the organization is fully populated.
|
||||
require.Equal(t, &codersdk.MinimalOrganization{
|
||||
ID: o.ID,
|
||||
Name: o.Name,
|
||||
DisplayName: o.DisplayName,
|
||||
Icon: o.Icon,
|
||||
}, alogs.AuditLogs[0].Organization)
|
||||
|
||||
// OrganizationID is deprecated, but make sure it is set.
|
||||
require.Equal(t, o.ID, alogs.AuditLogs[0].OrganizationID)
|
||||
|
||||
// Delete the org and try again, should be mostly empty.
|
||||
err = client.DeleteOrganization(ctx, o.ID.String())
|
||||
require.NoError(t, err)
|
||||
|
||||
alogs, err = client.AuditLogs(ctx, codersdk.AuditLogsRequest{
|
||||
Pagination: codersdk.Pagination{
|
||||
Limit: 1,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(1), alogs.Count)
|
||||
require.Len(t, alogs.AuditLogs, 1)
|
||||
|
||||
require.Equal(t, &codersdk.MinimalOrganization{
|
||||
ID: o.ID,
|
||||
}, alogs.AuditLogs[0].Organization)
|
||||
|
||||
// OrganizationID is deprecated, but make sure it is set.
|
||||
require.Equal(t, o.ID, alogs.AuditLogs[0].OrganizationID)
|
||||
|
||||
// Some audit entries do not have an organization at all, in which case the
|
||||
// response omits the organization.
|
||||
err = client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{
|
||||
ResourceType: codersdk.ResourceTypeAPIKey,
|
||||
ResourceID: user.UserID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
alogs, err = client.AuditLogs(ctx, codersdk.AuditLogsRequest{
|
||||
SearchQuery: "resource_type:api_key",
|
||||
Pagination: codersdk.Pagination{
|
||||
Limit: 1,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(1), alogs.Count)
|
||||
require.Len(t, alogs.AuditLogs, 1)
|
||||
|
||||
// The other will have no organization.
|
||||
require.Equal(t, (*codersdk.MinimalOrganization)(nil), alogs.AuditLogs[0].Organization)
|
||||
|
||||
// OrganizationID is deprecated, but make sure it is empty.
|
||||
require.Equal(t, uuid.Nil, alogs.AuditLogs[0].OrganizationID)
|
||||
})
|
||||
|
||||
t.Run("WorkspaceBuildAuditLink", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@ -159,8 +245,7 @@ func TestAuditLogs(t *testing.T) {
|
||||
|
||||
// Add an extra audit log in another organization
|
||||
err = client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{
|
||||
ResourceID: owner.UserID,
|
||||
OrganizationID: uuid.New(),
|
||||
ResourceID: owner.UserID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
|
@ -928,6 +928,16 @@ func (q *FakeQuerier) getLatestWorkspaceAppByTemplateIDUserIDSlugNoLock(ctx cont
|
||||
return database.WorkspaceApp{}, sql.ErrNoRows
|
||||
}
|
||||
|
||||
// getOrganizationByIDNoLock is used by other functions in the database fake.
|
||||
func (q *FakeQuerier) getOrganizationByIDNoLock(id uuid.UUID) (database.Organization, error) {
|
||||
for _, organization := range q.organizations {
|
||||
if organization.ID == id {
|
||||
return organization, nil
|
||||
}
|
||||
}
|
||||
return database.Organization{}, sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (*FakeQuerier) AcquireLock(_ context.Context, _ int64) error {
|
||||
return xerrors.New("AcquireLock must only be called within a transaction")
|
||||
}
|
||||
@ -2146,34 +2156,39 @@ func (q *FakeQuerier) GetAuditLogsOffset(_ context.Context, arg database.GetAudi
|
||||
user, err := q.getUserByIDNoLock(alog.UserID)
|
||||
userValid := err == nil
|
||||
|
||||
org, _ := q.getOrganizationByIDNoLock(alog.OrganizationID)
|
||||
|
||||
logs = append(logs, database.GetAuditLogsOffsetRow{
|
||||
ID: alog.ID,
|
||||
RequestID: alog.RequestID,
|
||||
OrganizationID: alog.OrganizationID,
|
||||
Ip: alog.Ip,
|
||||
UserAgent: alog.UserAgent,
|
||||
ResourceType: alog.ResourceType,
|
||||
ResourceID: alog.ResourceID,
|
||||
ResourceTarget: alog.ResourceTarget,
|
||||
ResourceIcon: alog.ResourceIcon,
|
||||
Action: alog.Action,
|
||||
Diff: alog.Diff,
|
||||
StatusCode: alog.StatusCode,
|
||||
AdditionalFields: alog.AdditionalFields,
|
||||
UserID: alog.UserID,
|
||||
UserUsername: sql.NullString{String: user.Username, Valid: userValid},
|
||||
UserName: sql.NullString{String: user.Name, Valid: userValid},
|
||||
UserEmail: sql.NullString{String: user.Email, Valid: userValid},
|
||||
UserCreatedAt: sql.NullTime{Time: user.CreatedAt, Valid: userValid},
|
||||
UserUpdatedAt: sql.NullTime{Time: user.UpdatedAt, Valid: userValid},
|
||||
UserLastSeenAt: sql.NullTime{Time: user.LastSeenAt, Valid: userValid},
|
||||
UserLoginType: database.NullLoginType{LoginType: user.LoginType, Valid: userValid},
|
||||
UserDeleted: sql.NullBool{Bool: user.Deleted, Valid: userValid},
|
||||
UserThemePreference: sql.NullString{String: user.ThemePreference, Valid: userValid},
|
||||
UserQuietHoursSchedule: sql.NullString{String: user.QuietHoursSchedule, Valid: userValid},
|
||||
UserStatus: database.NullUserStatus{UserStatus: user.Status, Valid: userValid},
|
||||
UserRoles: user.RBACRoles,
|
||||
Count: 0,
|
||||
ID: alog.ID,
|
||||
RequestID: alog.RequestID,
|
||||
OrganizationID: alog.OrganizationID,
|
||||
OrganizationName: org.Name,
|
||||
OrganizationDisplayName: org.DisplayName,
|
||||
OrganizationIcon: org.Icon,
|
||||
Ip: alog.Ip,
|
||||
UserAgent: alog.UserAgent,
|
||||
ResourceType: alog.ResourceType,
|
||||
ResourceID: alog.ResourceID,
|
||||
ResourceTarget: alog.ResourceTarget,
|
||||
ResourceIcon: alog.ResourceIcon,
|
||||
Action: alog.Action,
|
||||
Diff: alog.Diff,
|
||||
StatusCode: alog.StatusCode,
|
||||
AdditionalFields: alog.AdditionalFields,
|
||||
UserID: alog.UserID,
|
||||
UserUsername: sql.NullString{String: user.Username, Valid: userValid},
|
||||
UserName: sql.NullString{String: user.Name, Valid: userValid},
|
||||
UserEmail: sql.NullString{String: user.Email, Valid: userValid},
|
||||
UserCreatedAt: sql.NullTime{Time: user.CreatedAt, Valid: userValid},
|
||||
UserUpdatedAt: sql.NullTime{Time: user.UpdatedAt, Valid: userValid},
|
||||
UserLastSeenAt: sql.NullTime{Time: user.LastSeenAt, Valid: userValid},
|
||||
UserLoginType: database.NullLoginType{LoginType: user.LoginType, Valid: userValid},
|
||||
UserDeleted: sql.NullBool{Bool: user.Deleted, Valid: userValid},
|
||||
UserThemePreference: sql.NullString{String: user.ThemePreference, Valid: userValid},
|
||||
UserQuietHoursSchedule: sql.NullString{String: user.QuietHoursSchedule, Valid: userValid},
|
||||
UserStatus: database.NullUserStatus{UserStatus: user.Status, Valid: userValid},
|
||||
UserRoles: user.RBACRoles,
|
||||
Count: 0,
|
||||
})
|
||||
|
||||
if len(logs) >= int(arg.LimitOpt) {
|
||||
@ -2969,12 +2984,7 @@ func (q *FakeQuerier) GetOrganizationByID(_ context.Context, id uuid.UUID) (data
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
for _, organization := range q.organizations {
|
||||
if organization.ID == id {
|
||||
return organization, nil
|
||||
}
|
||||
}
|
||||
return database.Organization{}, sql.ErrNoRows
|
||||
return q.getOrganizationByIDNoLock(id)
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) GetOrganizationByName(_ context.Context, name string) (database.Organization, error) {
|
||||
|
@ -459,6 +459,9 @@ SELECT
|
||||
users.deleted AS user_deleted,
|
||||
users.theme_preference AS user_theme_preference,
|
||||
users.quiet_hours_schedule AS user_quiet_hours_schedule,
|
||||
COALESCE(organizations.name, '') AS organization_name,
|
||||
COALESCE(organizations.display_name, '') AS organization_display_name,
|
||||
COALESCE(organizations.icon, '') AS organization_icon,
|
||||
COUNT(audit_logs.*) OVER () AS count
|
||||
FROM
|
||||
audit_logs
|
||||
@ -487,6 +490,7 @@ FROM
|
||||
workspaces.id = workspace_builds.workspace_id AND
|
||||
workspace_builds.build_number = 1
|
||||
)
|
||||
LEFT JOIN organizations ON audit_logs.organization_id = organizations.id
|
||||
WHERE
|
||||
-- Filter resource_type
|
||||
CASE
|
||||
@ -582,35 +586,38 @@ type GetAuditLogsOffsetParams struct {
|
||||
}
|
||||
|
||||
type GetAuditLogsOffsetRow struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
Time time.Time `db:"time" json:"time"`
|
||||
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
||||
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
|
||||
Ip pqtype.Inet `db:"ip" json:"ip"`
|
||||
UserAgent sql.NullString `db:"user_agent" json:"user_agent"`
|
||||
ResourceType ResourceType `db:"resource_type" json:"resource_type"`
|
||||
ResourceID uuid.UUID `db:"resource_id" json:"resource_id"`
|
||||
ResourceTarget string `db:"resource_target" json:"resource_target"`
|
||||
Action AuditAction `db:"action" json:"action"`
|
||||
Diff json.RawMessage `db:"diff" json:"diff"`
|
||||
StatusCode int32 `db:"status_code" json:"status_code"`
|
||||
AdditionalFields json.RawMessage `db:"additional_fields" json:"additional_fields"`
|
||||
RequestID uuid.UUID `db:"request_id" json:"request_id"`
|
||||
ResourceIcon string `db:"resource_icon" json:"resource_icon"`
|
||||
UserUsername sql.NullString `db:"user_username" json:"user_username"`
|
||||
UserName sql.NullString `db:"user_name" json:"user_name"`
|
||||
UserEmail sql.NullString `db:"user_email" json:"user_email"`
|
||||
UserCreatedAt sql.NullTime `db:"user_created_at" json:"user_created_at"`
|
||||
UserUpdatedAt sql.NullTime `db:"user_updated_at" json:"user_updated_at"`
|
||||
UserLastSeenAt sql.NullTime `db:"user_last_seen_at" json:"user_last_seen_at"`
|
||||
UserStatus NullUserStatus `db:"user_status" json:"user_status"`
|
||||
UserLoginType NullLoginType `db:"user_login_type" json:"user_login_type"`
|
||||
UserRoles pq.StringArray `db:"user_roles" json:"user_roles"`
|
||||
UserAvatarUrl sql.NullString `db:"user_avatar_url" json:"user_avatar_url"`
|
||||
UserDeleted sql.NullBool `db:"user_deleted" json:"user_deleted"`
|
||||
UserThemePreference sql.NullString `db:"user_theme_preference" json:"user_theme_preference"`
|
||||
UserQuietHoursSchedule sql.NullString `db:"user_quiet_hours_schedule" json:"user_quiet_hours_schedule"`
|
||||
Count int64 `db:"count" json:"count"`
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
Time time.Time `db:"time" json:"time"`
|
||||
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
||||
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
|
||||
Ip pqtype.Inet `db:"ip" json:"ip"`
|
||||
UserAgent sql.NullString `db:"user_agent" json:"user_agent"`
|
||||
ResourceType ResourceType `db:"resource_type" json:"resource_type"`
|
||||
ResourceID uuid.UUID `db:"resource_id" json:"resource_id"`
|
||||
ResourceTarget string `db:"resource_target" json:"resource_target"`
|
||||
Action AuditAction `db:"action" json:"action"`
|
||||
Diff json.RawMessage `db:"diff" json:"diff"`
|
||||
StatusCode int32 `db:"status_code" json:"status_code"`
|
||||
AdditionalFields json.RawMessage `db:"additional_fields" json:"additional_fields"`
|
||||
RequestID uuid.UUID `db:"request_id" json:"request_id"`
|
||||
ResourceIcon string `db:"resource_icon" json:"resource_icon"`
|
||||
UserUsername sql.NullString `db:"user_username" json:"user_username"`
|
||||
UserName sql.NullString `db:"user_name" json:"user_name"`
|
||||
UserEmail sql.NullString `db:"user_email" json:"user_email"`
|
||||
UserCreatedAt sql.NullTime `db:"user_created_at" json:"user_created_at"`
|
||||
UserUpdatedAt sql.NullTime `db:"user_updated_at" json:"user_updated_at"`
|
||||
UserLastSeenAt sql.NullTime `db:"user_last_seen_at" json:"user_last_seen_at"`
|
||||
UserStatus NullUserStatus `db:"user_status" json:"user_status"`
|
||||
UserLoginType NullLoginType `db:"user_login_type" json:"user_login_type"`
|
||||
UserRoles pq.StringArray `db:"user_roles" json:"user_roles"`
|
||||
UserAvatarUrl sql.NullString `db:"user_avatar_url" json:"user_avatar_url"`
|
||||
UserDeleted sql.NullBool `db:"user_deleted" json:"user_deleted"`
|
||||
UserThemePreference sql.NullString `db:"user_theme_preference" json:"user_theme_preference"`
|
||||
UserQuietHoursSchedule sql.NullString `db:"user_quiet_hours_schedule" json:"user_quiet_hours_schedule"`
|
||||
OrganizationName string `db:"organization_name" json:"organization_name"`
|
||||
OrganizationDisplayName string `db:"organization_display_name" json:"organization_display_name"`
|
||||
OrganizationIcon string `db:"organization_icon" json:"organization_icon"`
|
||||
Count int64 `db:"count" json:"count"`
|
||||
}
|
||||
|
||||
// GetAuditLogsBefore retrieves `row_limit` number of audit logs before the provided
|
||||
@ -667,6 +674,9 @@ func (q *sqlQuerier) GetAuditLogsOffset(ctx context.Context, arg GetAuditLogsOff
|
||||
&i.UserDeleted,
|
||||
&i.UserThemePreference,
|
||||
&i.UserQuietHoursSchedule,
|
||||
&i.OrganizationName,
|
||||
&i.OrganizationDisplayName,
|
||||
&i.OrganizationIcon,
|
||||
&i.Count,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
|
@ -18,6 +18,9 @@ SELECT
|
||||
users.deleted AS user_deleted,
|
||||
users.theme_preference AS user_theme_preference,
|
||||
users.quiet_hours_schedule AS user_quiet_hours_schedule,
|
||||
COALESCE(organizations.name, '') AS organization_name,
|
||||
COALESCE(organizations.display_name, '') AS organization_display_name,
|
||||
COALESCE(organizations.icon, '') AS organization_icon,
|
||||
COUNT(audit_logs.*) OVER () AS count
|
||||
FROM
|
||||
audit_logs
|
||||
@ -46,6 +49,7 @@ FROM
|
||||
workspaces.id = workspace_builds.workspace_id AND
|
||||
workspace_builds.build_number = 1
|
||||
)
|
||||
LEFT JOIN organizations ON audit_logs.organization_id = organizations.id
|
||||
WHERE
|
||||
-- Filter resource_type
|
||||
CASE
|
||||
|
@ -315,11 +315,13 @@ func (api *API) deleteOrganization(rw http.ResponseWriter, r *http.Request) {
|
||||
// convertOrganization consumes the database representation and outputs an API friendly representation.
|
||||
func convertOrganization(organization database.Organization) codersdk.Organization {
|
||||
return codersdk.Organization{
|
||||
ID: organization.ID,
|
||||
Name: organization.Name,
|
||||
DisplayName: organization.DisplayName,
|
||||
MinimalOrganization: codersdk.MinimalOrganization{
|
||||
ID: organization.ID,
|
||||
Name: organization.Name,
|
||||
DisplayName: organization.DisplayName,
|
||||
Icon: organization.Icon,
|
||||
},
|
||||
Description: organization.Description,
|
||||
Icon: organization.Icon,
|
||||
CreatedAt: organization.CreatedAt,
|
||||
UpdatedAt: organization.UpdatedAt,
|
||||
IsDefault: organization.IsDefault,
|
||||
|
Reference in New Issue
Block a user