Skip to content
Merged
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
60 changes: 20 additions & 40 deletions api/core.openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -296,24 +296,16 @@ definitions:
type: object
PrivilegeKey:
enum:
- amie:read
- amie:write
- hpc:read
- hpc:write
- signer:read
- signer:write
- privileges:grant
- roles:manage
- core:clusters:read
- core:clusters:write
- core:privileges:grant
- core:roles:manage
type: string
x-enum-varnames:
- PrivilegeAMIERead
- PrivilegeAMIEWrite
- PrivilegeHPCRead
- PrivilegeHPCWrite
- PrivilegeSignerRead
- PrivilegeSignerWrite
- PrivilegeGrant
- PrivilegeRolesManage
- ClustersRead
- ClustersWrite
- PrivilegesGrant
- RolesManage
Project:
properties:
created_time:
Expand Down Expand Up @@ -2634,14 +2626,10 @@ paths:
parameters:
- description: Privilege key
enum:
- amie:read
- amie:write
- hpc:read
- hpc:write
- signer:read
- signer:write
- privileges:grant
- roles:manage
- core:clusters:read
- core:clusters:write
- core:privileges:grant
- core:roles:manage
in: path
name: key
required: true
Expand Down Expand Up @@ -3136,14 +3124,10 @@ paths:
type: string
- description: Privilege key
enum:
- amie:read
- amie:write
- hpc:read
- hpc:write
- signer:read
- signer:write
- privileges:grant
- roles:manage
- core:clusters:read
- core:clusters:write
- core:privileges:grant
- core:roles:manage
in: path
name: key
required: true
Expand Down Expand Up @@ -3664,14 +3648,10 @@ paths:
type: string
- description: Privilege key
enum:
- amie:read
- amie:write
- hpc:read
- hpc:write
- signer:read
- signer:write
- privileges:grant
- roles:manage
- core:clusters:read
- core:clusters:write
- core:privileges:grant
- core:roles:manage
in: path
name: key
required: true
Expand Down
25 changes: 12 additions & 13 deletions connectors/ACCESS/AMIE-Processor/server/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import (
"github.com/apache/airavata-custos/connectors/ACCESS/AMIE-Processor/store"
"github.com/apache/airavata-custos/pkg/common"
"github.com/apache/airavata-custos/pkg/identity"
"github.com/apache/airavata-custos/pkg/models"
)

type Handlers struct {
Expand All @@ -41,19 +40,19 @@ func NewHandlers(audits store.PacketAuditStore, packets store.PacketStore) *Hand
}

// RegisterRoutes attaches the AMIE connector's HTTP endpoints via router, gated
// on amie:read for query routes and amie:write for retry/resolve/link.
// by scope-specific read/write privileges.
func (h *Handlers) RegisterRoutes(router *identity.Router) {
router.RequirePrivilege("GET /connectors/amie/packets", models.PrivilegeAMIERead, h.listPackets)
router.RequirePrivilege("GET /connectors/amie/packets/{id}", models.PrivilegeAMIERead, h.getPacket)
router.RequirePrivilege("GET /connectors/amie/packets/{id}/events", models.PrivilegeAMIERead, h.listPacketEvents)
router.RequirePrivilege("GET /connectors/amie/packets/{packet_id}/audits", models.PrivilegeAMIERead, h.listPacketAudits)
router.RequirePrivilege("GET /connectors/amie/stats", models.PrivilegeAMIERead, h.getStats)
router.RequirePrivilege("GET /connectors/amie/replies", models.PrivilegeAMIERead, h.listReplies)
router.RequirePrivilege("GET /connectors/amie/unmapped", models.PrivilegeAMIERead, h.listUnmapped)
router.RequirePrivilege("POST /connectors/amie/packets/{id}/retry", models.PrivilegeAMIEWrite, h.retryPacket)
router.RequirePrivilege("POST /connectors/amie/packets/{id}/resolve", models.PrivilegeAMIEWrite, h.resolvePacket)
router.RequirePrivilege("POST /connectors/amie/replies/{id}/retry", models.PrivilegeAMIEWrite, h.retryReply)
router.RequirePrivilege("POST /connectors/amie/unmapped/{id}/link", models.PrivilegeAMIEWrite, h.linkUnmapped)
router.RequirePrivilege("GET /connectors/amie/packets", PacketsRead, h.listPackets)
router.RequirePrivilege("GET /connectors/amie/packets/{id}", PacketsRead, h.getPacket)
router.RequirePrivilege("GET /connectors/amie/packets/{id}/events", PacketsRead, h.listPacketEvents)
router.RequirePrivilege("GET /connectors/amie/packets/{packet_id}/audits", PacketsRead, h.listPacketAudits)
router.RequirePrivilege("GET /connectors/amie/stats", PacketsRead, h.getStats)
router.RequirePrivilege("GET /connectors/amie/replies", RepliesRead, h.listReplies)
router.RequirePrivilege("GET /connectors/amie/unmapped", UnmappedRead, h.listUnmapped)
router.RequirePrivilege("POST /connectors/amie/packets/{id}/retry", PacketsWrite, h.retryPacket)
router.RequirePrivilege("POST /connectors/amie/packets/{id}/resolve", PacketsWrite, h.resolvePacket)
router.RequirePrivilege("POST /connectors/amie/replies/{id}/retry", RepliesWrite, h.retryReply)
router.RequirePrivilege("POST /connectors/amie/unmapped/{id}/link", UnmappedWrite, h.linkUnmapped)
}

// @Summary List audit events for an AMIE packet
Expand Down
6 changes: 5 additions & 1 deletion connectors/ACCESS/AMIE-Processor/server/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,11 @@ func newTestServer(t *testing.T, store *fakePacketAuditStore) *httptest.Server {
// Attach the caller and required privileges to ctx since the auth middleware isn't running here.
wrap := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := identity.WithCaller(r.Context(), &identity.Caller{UserID: "test-user"})
ctx = identity.WithPrivilegesForTest(ctx, []models.PrivilegeKey{models.PrivilegeAMIERead, models.PrivilegeAMIEWrite})
ctx = identity.WithPrivilegesForTest(ctx, []models.PrivilegeKey{
PacketsRead, PacketsWrite,
RepliesRead, RepliesWrite,
UnmappedRead, UnmappedWrite,
})
mux.ServeHTTP(w, r.WithContext(ctx))
})
srv := httptest.NewServer(wrap)
Expand Down
37 changes: 37 additions & 0 deletions connectors/ACCESS/AMIE-Processor/server/privileges.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// 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 server

import "github.com/apache/airavata-custos/pkg/models"

const (
PacketsRead models.PrivilegeKey = "amie:packets:read"
PacketsWrite models.PrivilegeKey = "amie:packets:write"
RepliesRead models.PrivilegeKey = "amie:replies:read"
RepliesWrite models.PrivilegeKey = "amie:replies:write"
UnmappedRead models.PrivilegeKey = "amie:unmapped:read"
UnmappedWrite models.PrivilegeKey = "amie:unmapped:write"
)

func init() {
models.Register(
PacketsRead, PacketsWrite,
RepliesRead, RepliesWrite,
UnmappedRead, UnmappedWrite,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -247,14 +247,16 @@ seed_rows:
compute_allocation_membership_id: { from: "compute_allocation_memberships.id where compute_allocation_id=(compute_allocations.id where name=BL-001)" } }

user_privileges:
minimum_count: 6 # dev-admin gets all 6 portal privileges via seed
minimum_count: 8 # dev-admin gets all portal privileges via seed
rows:
- { user_id: dev-admin, privilege: "amie:read" }
- { user_id: dev-admin, privilege: "amie:write" }
- { user_id: dev-admin, privilege: "hpc:read" }
- { user_id: dev-admin, privilege: "hpc:write" }
- { user_id: dev-admin, privilege: "signer:read" }
- { user_id: dev-admin, privilege: "signer:write" }
- { user_id: dev-admin, privilege: "amie:packets:read" }
- { user_id: dev-admin, privilege: "amie:packets:write" }
- { user_id: dev-admin, privilege: "amie:replies:read" }
- { user_id: dev-admin, privilege: "amie:replies:write" }
- { user_id: dev-admin, privilege: "amie:unmapped:read" }
- { user_id: dev-admin, privilege: "amie:unmapped:write" }
- { user_id: dev-admin, privilege: "core:clusters:read" }
- { user_id: dev-admin, privilege: "core:clusters:write" }

verification_queries:

Expand Down
21 changes: 13 additions & 8 deletions dev-ops/compose/seeds/dev_users_and_roles.sql
Original file line number Diff line number Diff line change
Expand Up @@ -55,20 +55,25 @@ VALUES
INSERT IGNORE INTO roles (id, name, description, is_system)
VALUES
('11111111-1111-1111-1111-111111111111', 'operator', 'Day-to-day AMIE and HPC operations (read + write)', 0),
('22222222-2222-2222-2222-222222222222', 'auditor', 'Read-only access across AMIE, HPC, and signer surfaces', 0);
('22222222-2222-2222-2222-222222222222', 'auditor', 'Read-only access across AMIE and HPC surfaces', 0);

-- operator privileges
INSERT IGNORE INTO role_privileges (role_id, privilege) VALUES
('11111111-1111-1111-1111-111111111111', 'amie:read'),
('11111111-1111-1111-1111-111111111111', 'amie:write'),
('11111111-1111-1111-1111-111111111111', 'hpc:read'),
('11111111-1111-1111-1111-111111111111', 'hpc:write');
('11111111-1111-1111-1111-111111111111', 'amie:packets:read'),
('11111111-1111-1111-1111-111111111111', 'amie:packets:write'),
('11111111-1111-1111-1111-111111111111', 'amie:replies:read'),
('11111111-1111-1111-1111-111111111111', 'amie:replies:write'),
('11111111-1111-1111-1111-111111111111', 'amie:unmapped:read'),
('11111111-1111-1111-1111-111111111111', 'amie:unmapped:write'),
('11111111-1111-1111-1111-111111111111', 'core:clusters:read'),
('11111111-1111-1111-1111-111111111111', 'core:clusters:write');

-- auditor privileges
INSERT IGNORE INTO role_privileges (role_id, privilege) VALUES
('22222222-2222-2222-2222-222222222222', 'amie:read'),
('22222222-2222-2222-2222-222222222222', 'hpc:read'),
('22222222-2222-2222-2222-222222222222', 'signer:read');
('22222222-2222-2222-2222-222222222222', 'amie:packets:read'),
('22222222-2222-2222-2222-222222222222', 'amie:replies:read'),
('22222222-2222-2222-2222-222222222222', 'amie:unmapped:read'),
('22222222-2222-2222-2222-222222222222', 'core:clusters:read');

-- ---------------------------------------------------------------------------
-- Role assignments
Expand Down
67 changes: 67 additions & 0 deletions docs/privileges.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Privilege Reference

The authoritative list of privilege keys, their owning component, and the
endpoints they gate. Keep this in sync with `pkg/models/privilege.go` and
each connector's `privileges.go`.

## Key format

```
<owner>:<scope>:<action>
```

| Segment | Meaning | Examples |
|---|---|---|
| `owner` | The component that defines and consumes the privilege. `core` for the Custos core; the connector short name otherwise. | `core`, `amie` |
| `scope` | The resource the privilege applies to inside that owner. | `clusters`, `packets`, `replies` |
| `action` | The verb the privilege gates. Use `read` for reads, `write` for mutations, `grant` / `manage` for meta operations. | `read`, `write`, `grant`, `manage` |

A privilege key with a different shape (e.g. `amie:read`) is legacy and
should be migrated.

## Core (`pkg/models/privilege.go`)

| Privilege | Endpoints it gates |
|---|---|
| `core:clusters:read` | `GET /compute-clusters`, `GET /compute-clusters/{id}` |
| `core:clusters:write` | `POST /compute-clusters`, `PUT /compute-clusters/{id}`, `DELETE /compute-clusters/{id}` |
| `core:privileges:grant` | `GET /privileges/catalog`, `GET /users/{id}/privileges`, `GET /privileges/{key}/holders`, `POST /users/{id}/privileges`, `DELETE /users/{id}/privileges/{key}` |
| `core:roles:manage` | `GET /roles`, `POST /roles`, `PUT /roles/{id}`, `DELETE /roles/{id}`, and the role-assignment endpoints under `/users/{id}/roles` |

The two meta privileges (`core:privileges:grant`, `core:roles:manage`) are
the only ones the bootstrap super_admin role carries by default. Everything
else is granted by an admin holding one of them.

## AMIE connector (`connectors/ACCESS/AMIE-Processor/server/privileges.go`)

| Privilege | Endpoints it gates |
|---|---|
| `amie:packets:read` | `GET /connectors/amie/packets`, `GET /connectors/amie/packets/{id}`, `GET /connectors/amie/packets/{id}/events`, `GET /connectors/amie/packets/{id}/audits`, `GET /connectors/amie/stats` |
| `amie:packets:write` | `POST /connectors/amie/packets/{id}/retry`, `POST /connectors/amie/packets/{id}/resolve` |
| `amie:replies:read` | `GET /connectors/amie/replies` |
| `amie:replies:write` | `POST /connectors/amie/replies/{id}/retry` |
| `amie:unmapped:read` | `GET /connectors/amie/unmapped` |
| `amie:unmapped:write` | _(reserved, no endpoint yet)_ |

## Suggested role bundles

These bundles are conventions, not enforced. Use them as starting points
when defining roles in your deployment.

| Role | Privileges |
|---|---|
| `super_admin` (bootstrap) | `core:privileges:grant`, `core:roles:manage`. Plus the operational set if the dev seed is applied. |

## Adding a new privilege

1. Pick a key in the `<owner>:<scope>:<action>` shape.
2. Declare it in the owning package:
- Core: add a constant to `pkg/models/privilege.go` and register it
via `models.Register(...)`.
- Connector: add a constant to the connector's `server/privileges.go`
and register it in the connector's init path.
3. Gate the endpoint with `router.RequirePrivilege(pattern, KEY, handler)`.
4. Add a row to the appropriate table above.

The runtime privilege catalog (`GET /privileges/catalog`) returns whatever
the registry holds at startup, so registering is what makes a key real.
6 changes: 3 additions & 3 deletions internal/server/integration_common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,15 +119,15 @@ func seedUser(t *testing.T, database *sqlx.DB, email string) string {
return userID
}

// seedPrivilegeGrant directly inserts an active privileges:grant for userID.
// seedPrivilegesGrant directly inserts an active privileges:grant for userID.
// Bypasses the service guards so tests can stand up a granter without a
// chicken-and-egg dependency.
func seedPrivilegeGrant(t *testing.T, database *sqlx.DB, userID string) {
func seedPrivilegesGrant(t *testing.T, database *sqlx.DB, userID string) {
t.Helper()
if _, err := database.Exec(
`INSERT INTO user_privileges (id, user_id, privilege, granted_at, reason)
VALUES (?, ?, ?, NOW(6), 'seed')`,
uuid.NewString(), userID, string(models.PrivilegeGrant),
uuid.NewString(), userID, string(models.PrivilegesGrant),
); err != nil {
t.Fatalf("seed privileges:grant for %s: %v", userID, err)
}
Expand Down
Loading
Loading