fix: user grants deactivation (#8634)

# Which Problems Are Solved

ZITADEL's user grants deactivation mechanism did not work correctly.
Deactivated user grants were still provided in token, which could lead
to unauthorized access to applications and resources.
Additionally, the management and auth API always returned the state as
active or did not provide any information about the state.

# How the Problems Are Solved

- Correctly check the user grant state on active for tokens and user
information (userinfo, introspection, saml attributes)
- Map state in API and display in Console

(cherry picked from commit ca1914e235df8eb62189cec07eb0de2cdad29629)
This commit is contained in:
Livio Spring 2024-09-17 14:18:29 +02:00
parent 461f6bf3d3
commit 1b39d71921
No known key found for this signature in database
GPG Key ID: 26BB1C2FA5952CF0
15 changed files with 95 additions and 12 deletions

View File

@ -154,10 +154,25 @@
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="state">
<th mat-header-cell *matHeaderCellDef>{{ 'PROJECT.GRANT.STATE' | translate }}</th>
<td mat-cell *matCellDef="let grant">
<span
class="state"
[ngClass]="{
active: grant.state === UserGrantState.USER_GRANT_STATE_ACTIVE,
inactive: grant.state === UserGrantState.USER_GRANT_STATE_INACTIVE,
}"
>
{{ 'USER.DATA.STATE' + grant.state | translate }}
</span>
</td>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd> <ng-container matColumnDef="actions" stickyEnd>
<th mat-header-cell *matHeaderCellDef class="user-tr-actions"></th> <th mat-header-cell *matHeaderCellDef class="user-tr-actions"></th>
<td mat-cell class="user-tr-actions" *matCellDef="let grant; let i = index"> <td mat-cell class="user-tr-actions" *matCellDef="let grant; let i = index">
<cnsl-table-actions [hasActions]="true"> <cnsl-table-actions [hasActions]="!context.includes('user')">
<button <button
actions actions
matTooltip="{{ 'ACTIONS.REMOVE' | translate }}" matTooltip="{{ 'ACTIONS.REMOVE' | translate }}"

View File

@ -8,7 +8,13 @@ import { tap } from 'rxjs/operators';
import { enterAnimations } from 'src/app/animations'; import { enterAnimations } from 'src/app/animations';
import { UserGrant as AuthUserGrant } from 'src/app/proto/generated/zitadel/auth_pb'; import { UserGrant as AuthUserGrant } from 'src/app/proto/generated/zitadel/auth_pb';
import { Role } from 'src/app/proto/generated/zitadel/project_pb'; import { Role } from 'src/app/proto/generated/zitadel/project_pb';
import { Type, UserGrant as MgmtUserGrant, UserGrantQuery, UserGrant } from 'src/app/proto/generated/zitadel/user_pb'; import {
Type,
UserGrant as MgmtUserGrant,
UserGrant,
UserGrantQuery,
UserGrantState,
} from 'src/app/proto/generated/zitadel/user_pb';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service'; import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ManagementService } from 'src/app/services/mgmt.service'; import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
@ -66,6 +72,7 @@ export class UserGrantsComponent implements OnInit, AfterViewInit {
public UserGrantContext: any = UserGrantContext; public UserGrantContext: any = UserGrantContext;
public Type: any = Type; public Type: any = Type;
public ActionKeysType: any = ActionKeysType; public ActionKeysType: any = ActionKeysType;
public UserGrantState: any = UserGrantState;
@Input() public type: Type | undefined = undefined; @Input() public type: Type | undefined = undefined;
public filterOpen: boolean = false; public filterOpen: boolean = false;
@ -86,6 +93,7 @@ export class UserGrantsComponent implements OnInit, AfterViewInit {
'type', 'type',
'creationDate', 'creationDate',
'changeDate', 'changeDate',
'state',
'roleNamesList', 'roleNamesList',
'actions', 'actions',
]; ];

View File

@ -17,6 +17,7 @@
'type', 'type',
'creationDate', 'creationDate',
'changeDate', 'changeDate',
'state',
'roleNamesList', 'roleNamesList',
'actions' 'actions'
]" ]"

View File

@ -42,7 +42,16 @@
[context]="UserGrantContext.GRANTED_PROJECT" [context]="UserGrantContext.GRANTED_PROJECT"
[projectId]="projectId" [projectId]="projectId"
[grantId]="grantId" [grantId]="grantId"
[displayedColumns]="['select', 'user', 'projectId', 'creationDate', 'changeDate', 'roleNamesList', 'actions']" [displayedColumns]="[
'select',
'user',
'projectId',
'creationDate',
'changeDate',
'state',
'roleNamesList',
'actions',
]"
[disableWrite]="(['user.grant.write$', 'user.grant.write:' + grantId] | hasRole | async) === false" [disableWrite]="(['user.grant.write$', 'user.grant.write:' + grantId] | hasRole | async) === false"
[disableDelete]="(['user.grant.delete$', 'user.grant.delete:' + grantId] | hasRole | async) === false" [disableDelete]="(['user.grant.delete$', 'user.grant.delete:' + grantId] | hasRole | async) === false"
[refreshOnPreviousRoutes]="['/grant-create/project/{{projectId}}/grant/{{grantId}}']" [refreshOnPreviousRoutes]="['/grant-create/project/{{projectId}}/grant/{{grantId}}']"

View File

@ -187,7 +187,7 @@
<cnsl-user-grants <cnsl-user-grants
[context]="UserGrantContext.OWNED_PROJECT" [context]="UserGrantContext.OWNED_PROJECT"
[projectId]="projectId" [projectId]="projectId"
[displayedColumns]="['select', 'user', 'creationDate', 'changeDate', 'roleNamesList', 'actions']" [displayedColumns]="['select', 'user', 'creationDate', 'changeDate', 'state', 'roleNamesList', 'actions']"
[refreshOnPreviousRoutes]="['/grant-create/project/' + projectId]" [refreshOnPreviousRoutes]="['/grant-create/project/' + projectId]"
[disableWrite]="(['user.grant.write$', 'user.grant.write:' + projectId] | hasRole | async) === false" [disableWrite]="(['user.grant.write$', 'user.grant.write:' + projectId] | hasRole | async) === false"
[disableDelete]="(['user.grant.delete$', 'user.grant.delete:' + projectId] | hasRole | async) === false" [disableDelete]="(['user.grant.delete$', 'user.grant.delete:' + projectId] | hasRole | async) === false"

View File

@ -136,7 +136,16 @@
<cnsl-user-grants <cnsl-user-grants
[userId]="user.id" [userId]="user.id"
[context]="USERGRANTCONTEXT" [context]="USERGRANTCONTEXT"
[displayedColumns]="['org', 'projectId', 'type', 'creationDate', 'changeDate', 'roleNamesList', 'actions']" [displayedColumns]="[
'org',
'projectId',
'type',
'creationDate',
'changeDate',
'state',
'roleNamesList',
'actions',
]"
[disableWrite]="(['user.grant.write$'] | hasRole | async) === false" [disableWrite]="(['user.grant.write$'] | hasRole | async) === false"
[disableDelete]="(['user.grant.delete$'] | hasRole | async) === false" [disableDelete]="(['user.grant.delete$'] | hasRole | async) === false"
> >

View File

@ -222,7 +222,7 @@
<cnsl-user-grants <cnsl-user-grants
[userId]="user.id" [userId]="user.id"
[context]="USERGRANTCONTEXT" [context]="USERGRANTCONTEXT"
[displayedColumns]="['select', 'projectId', 'creationDate', 'changeDate', 'roleNamesList', 'actions']" [displayedColumns]="['select', 'projectId', 'creationDate', 'changeDate', 'state', 'roleNamesList', 'actions']"
[disableWrite]="(['user.grant.write$'] | hasRole | async) === false" [disableWrite]="(['user.grant.write$'] | hasRole | async) === false"
[disableDelete]="(['user.grant.delete$'] | hasRole | async) === false" [disableDelete]="(['user.grant.delete$'] | hasRole | async) === false"
> >

View File

@ -55,5 +55,6 @@ func UserGrantToPb(grant *query.UserGrant) *auth_pb.UserGrant {
ProjectGrantId: grant.GrantID, ProjectGrantId: grant.GrantID,
RoleKeys: grant.Roles, RoleKeys: grant.Roles,
UserType: user.TypeToPb(grant.UserType), UserType: user.TypeToPb(grant.UserType),
State: user.UserGrantStateToPb(grant.State),
} }
} }

View File

@ -23,7 +23,7 @@ func UserGrantToPb(assetPrefix string, grant *query.UserGrant) *user_pb.UserGran
return &user_pb.UserGrant{ return &user_pb.UserGrant{
Id: grant.ID, Id: grant.ID,
UserId: grant.UserID, UserId: grant.UserID,
State: user_pb.UserGrantState_USER_GRANT_STATE_ACTIVE, State: UserGrantStateToPb(grant.State),
RoleKeys: grant.Roles, RoleKeys: grant.Roles,
ProjectId: grant.ProjectID, ProjectId: grant.ProjectID,
OrgId: grant.ResourceOwner, OrgId: grant.ResourceOwner,
@ -51,6 +51,21 @@ func UserGrantToPb(assetPrefix string, grant *query.UserGrant) *user_pb.UserGran
} }
} }
func UserGrantStateToPb(state domain.UserGrantState) user_pb.UserGrantState {
switch state {
case domain.UserGrantStateActive:
return user_pb.UserGrantState_USER_GRANT_STATE_ACTIVE
case domain.UserGrantStateInactive:
return user_pb.UserGrantState_USER_GRANT_STATE_INACTIVE
case domain.UserGrantStateRemoved,
domain.UserGrantStateUnspecified:
// these states should never occur here and are mainly listed for linting purposes
fallthrough
default:
return user_pb.UserGrantState_USER_GRANT_STATE_UNSPECIFIED
}
}
func UserGrantQueriesToQuery(ctx context.Context, queries []*user_pb.UserGrantQuery) (q []query.SearchQuery, err error) { func UserGrantQueriesToQuery(ctx context.Context, queries []*user_pb.UserGrantQuery) (q []query.SearchQuery, err error) {
q = make([]query.SearchQuery, len(queries)) q = make([]query.SearchQuery, len(queries))
for i, query := range queries { for i, query := range queries {

View File

@ -799,19 +799,24 @@ func (o *OPStorage) assertRoles(ctx context.Context, userID, applicationID strin
if projectID != "" { if projectID != "" {
roleAudience = append(roleAudience, projectID) roleAudience = append(roleAudience, projectID)
} }
queries := make([]query.SearchQuery, 0, 2)
projectQuery, err := query.NewUserGrantProjectIDsSearchQuery(roleAudience) projectQuery, err := query.NewUserGrantProjectIDsSearchQuery(roleAudience)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
queries = append(queries, projectQuery)
userIDQuery, err := query.NewUserGrantUserIDSearchQuery(userID) userIDQuery, err := query.NewUserGrantUserIDSearchQuery(userID)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
queries = append(queries, userIDQuery) activeQuery, err := query.NewUserGrantStateQuery(domain.UserGrantStateActive)
if err != nil {
return nil, nil, err
}
grants, err := o.query.UserGrants(ctx, &query.UserGrantsQueries{ grants, err := o.query.UserGrants(ctx, &query.UserGrantsQueries{
Queries: queries, Queries: []query.SearchQuery{
projectQuery,
userIDQuery,
activeQuery,
},
}, true) }, true)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err

View File

@ -324,10 +324,15 @@ func (p *Storage) getGrants(ctx context.Context, userID, applicationID string) (
if err != nil { if err != nil {
return nil, err return nil, err
} }
activeQuery, err := query.NewUserGrantStateQuery(domain.UserGrantStateActive)
if err != nil {
return nil, err
}
return p.query.UserGrants(ctx, &query.UserGrantsQueries{ return p.query.UserGrants(ctx, &query.UserGrantsQueries{
Queries: []query.SearchQuery{ Queries: []query.SearchQuery{
projectQuery, projectQuery,
userIDQuery, userIDQuery,
activeQuery,
}, },
}, true) }, true)
} }

View File

@ -11,6 +11,7 @@ import (
sd "github.com/zitadel/zitadel/internal/config/systemdefaults" sd "github.com/zitadel/zitadel/internal/config/systemdefaults"
"github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/domain"
eventstore2 "github.com/zitadel/zitadel/internal/eventstore" eventstore2 "github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/id" "github.com/zitadel/zitadel/internal/id"
"github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/query"
@ -118,7 +119,11 @@ func (q queryViewWrapper) UserGrantsByProjectAndUserID(ctx context.Context, proj
if err != nil { if err != nil {
return nil, err return nil, err
} }
queries := &query.UserGrantsQueries{Queries: []query.SearchQuery{userGrantUserID, userGrantProjectID}} activeQuery, err := query.NewUserGrantStateQuery(domain.UserGrantStateActive)
if err != nil {
return nil, err
}
queries := &query.UserGrantsQueries{Queries: []query.SearchQuery{userGrantUserID, userGrantProjectID, activeQuery}}
grants, err := q.Queries.UserGrants(ctx, queries, true) grants, err := q.Queries.UserGrants(ctx, queries, true)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -143,6 +143,10 @@ func NewUserGrantRoleQuery(value string) (SearchQuery, error) {
return NewTextQuery(UserGrantRoles, value, TextListContains) return NewTextQuery(UserGrantRoles, value, TextListContains)
} }
func NewUserGrantStateQuery(value domain.UserGrantState) (SearchQuery, error) {
return NewNumberQuery(UserGrantState, value, NumberEquals)
}
func NewUserGrantWithGrantedQuery(owner string) (SearchQuery, error) { func NewUserGrantWithGrantedQuery(owner string) (SearchQuery, error) {
orgQuery, err := NewUserGrantResourceOwnerSearchQuery(owner) orgQuery, err := NewUserGrantResourceOwnerSearchQuery(owner)
if err != nil { if err != nil {

View File

@ -38,6 +38,7 @@ user_grants as (
where user_id = $1 where user_id = $1
and instance_id = $2 and instance_id = $2
and project_id = any($3) and project_id = any($3)
and state = 1
{{ if . -}} {{ if . -}}
and resource_owner = any($4) and resource_owner = any($4)
{{- end }} {{- end }}

View File

@ -1565,6 +1565,11 @@ message UserGrant {
description: "type of the user (human / machine)" description: "type of the user (human / machine)"
} }
]; ];
zitadel.user.v1.UserGrantState state = 13 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "current state of the user grant";
}
];
} }
message ListMyProjectOrgsRequest { message ListMyProjectOrgsRequest {