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
4 changes: 2 additions & 2 deletions charts/identity/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ apiVersion: v2
name: identity
description: Helm chart for deploying the IdentityService gRPC server.
type: application
version: 0.1.0
appVersion: 0.1.0
version: 0.2.0
appVersion: 0.2.0
home: https://github.com/agynio/identity
sources:
- https://github.com/agynio/identity
Expand Down
31 changes: 31 additions & 0 deletions charts/identity/templates/authorization-policy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{{- if .Values.internalAuthorizationPolicy.enabled }}
{{- $namespace := .Release.Namespace }}
# DeleteOrganizationResources is one step of the organization teardown. It has
# no caller identity to authorize, so who may reach it at all is settled here.
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: {{ .Release.Name }}-internal-identity
labels:
app.kubernetes.io/name: {{ include "service-base.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
spec:
selector:
matchLabels:
app.kubernetes.io/name: {{ include "service-base.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
action: DENY
rules:
- from:
- source:
notPrincipals:
{{- range .Values.internalAuthorizationPolicy.serviceAccounts }}
- "cluster.local/ns/{{ .namespace | default $namespace }}/sa/{{ .name }}"
{{- end }}
to:
- operation:
methods:
- POST
paths:
- /agynio.api.identity.v1.IdentityService/DeleteOrganizationResources
{{- end }}
6 changes: 6 additions & 0 deletions charts/identity/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,9 @@ metrics:
path: /metrics
interval: ""
scrapeTimeout: ""

internalAuthorizationPolicy:
enabled: true
serviceAccounts:
- name: organizations
namespace: ""
19 changes: 19 additions & 0 deletions internal/server/nicknames.go
Original file line number Diff line number Diff line change
Expand Up @@ -377,3 +377,22 @@
}
return nil
}

// DeleteOrganizationResources removes the organization's nicknames. It is
// internal: Istio settles who may call it, so there is no permission check and
// no caller identity to check against. Step 8 of the organization teardown,
// last because the identities that held these nicknames are deleted by the
// services that registered them, in the steps above.
//
// Idempotent: the delete is unconditional, so a retried step finds nothing to
// remove and still succeeds.
func (s *Server) DeleteOrganizationResources(ctx context.Context, req *identityv1.DeleteOrganizationResourcesRequest) (*identityv1.DeleteOrganizationResourcesResponse, error) {

Check failure on line 389 in internal/server/nicknames.go

View workflow job for this annotation

GitHub Actions / e2e

undefined: identityv1.DeleteOrganizationResourcesResponse

Check failure on line 389 in internal/server/nicknames.go

View workflow job for this annotation

GitHub Actions / e2e

undefined: identityv1.DeleteOrganizationResourcesRequest
organizationID, err := parseUUID(req.GetOrganizationId())
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "organization_id: %v", err)
}
if err := s.store.RemoveOrganizationNicknames(ctx, organizationID); err != nil {
return nil, status.Errorf(codes.Internal, "remove organization nicknames: %v", err)
}
return &identityv1.DeleteOrganizationResourcesResponse{}, nil

Check failure on line 397 in internal/server/nicknames.go

View workflow job for this annotation

GitHub Actions / e2e

undefined: identityv1.DeleteOrganizationResourcesResponse
}
36 changes: 36 additions & 0 deletions internal/server/nicknames_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ type fakeStore struct {
lastInstanceSuffix *string
resolution store.NicknameResolution
registerErr error
removedOrgs []uuid.UUID
removeOrgErr error
}

func (f *fakeStore) RegisterIdentity(context.Context, uuid.UUID, int16) error {
Expand Down Expand Up @@ -93,6 +95,14 @@ func (f *fakeStore) ResolveNickname(_ context.Context, _ uuid.UUID, nickname str
return f.resolution, nil
}

func (f *fakeStore) RemoveOrganizationNicknames(_ context.Context, organizationID uuid.UUID) error {
if f.removeOrgErr != nil {
return f.removeOrgErr
}
f.removedOrgs = append(f.removedOrgs, organizationID)
return nil
}

func (f *fakeStore) BatchGetNicknames(_ context.Context, organizationID uuid.UUID, identityIDs []uuid.UUID) (map[uuid.UUID]store.NicknameEntry, error) {
f.lastOrganizationID = organizationID
f.lastIdentityIDs = identityIDs
Expand Down Expand Up @@ -306,3 +316,29 @@ func TestBatchGetNicknamesReturnsInstanceSuffix(t *testing.T) {
require.Equal(t, "bob", response.Entries[0].GetNickname())
require.Equal(t, instanceSuffix, response.Entries[0].GetInstanceSuffix())
}

func TestDeleteOrganizationResourcesRemovesNicknames(t *testing.T) {
organizationID := uuid.New()
fake := &fakeStore{}
server := New(fake, &fakeAuthClient{})

req := &identityv1.DeleteOrganizationResourcesRequest{OrganizationId: organizationID.String()}
// Internal RPC: no identity in the context, and none required.
_, err := server.DeleteOrganizationResources(context.Background(), req)
require.NoError(t, err)

// The cascade retries a step it is unsure finished, so a second call has to
// succeed on the now-empty organization rather than report NotFound the way
// RemoveNickname would.
_, err = server.DeleteOrganizationResources(context.Background(), req)
require.NoError(t, err)
require.Equal(t, []uuid.UUID{organizationID, organizationID}, fake.removedOrgs)
}

func TestDeleteOrganizationResourcesRejectsInvalidOrganizationID(t *testing.T) {
server := New(&fakeStore{}, &fakeAuthClient{})
_, err := server.DeleteOrganizationResources(context.Background(), &identityv1.DeleteOrganizationResourcesRequest{
OrganizationId: "not-a-uuid",
})
require.Equal(t, codes.InvalidArgument, status.Code(err))
}
1 change: 1 addition & 0 deletions internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type identityStore interface {
RemoveNickname(context.Context, uuid.UUID, uuid.UUID, *uuid.UUID) error
ResolveNickname(context.Context, uuid.UUID, string, *string) (store.NicknameResolution, error)
BatchGetNicknames(context.Context, uuid.UUID, []uuid.UUID) (map[uuid.UUID]store.NicknameEntry, error)
RemoveOrganizationNicknames(context.Context, uuid.UUID) error
}

type authorizationChecker interface {
Expand Down
8 changes: 8 additions & 0 deletions internal/store/nicknames.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@ func (s *Store) RemoveNickname(ctx context.Context, organizationID uuid.UUID, id
return nil
}

// RemoveOrganizationNicknames drops every nickname held in the organization.
// Unlike RemoveNickname it does not report an empty result as NotFound: the
// teardown step it serves is retried, and finding nothing left is success.
func (s *Store) RemoveOrganizationNicknames(ctx context.Context, organizationID uuid.UUID) error {
_, err := s.pool.Exec(ctx, `DELETE FROM org_nicknames WHERE organization_id = $1`, organizationID)
return err
}

func (s *Store) ResolveNickname(ctx context.Context, organizationID uuid.UUID, nickname string, instanceSuffix *string) (NicknameResolution, error) {
var resolution NicknameResolution
var installationID pgtype.UUID
Expand Down
Loading